Libraries

First we’ll load in the packages we need to tidy & analyze our simulation results.

library(dplyr)       # data manipulation 
library(Seurat)      # scRNA-seq tools & data structures
library(ggplot2)     # plots
library(targets)     # pipeline tools
library(paletteer)   # plot colors 
library(patchwork)   # plot layouts
library(reticulate)  # python

Data

Next we’ll load in the clustering algorithm outputs from out {targets} pipeline.

# pancreas reference 
tar_load(clustres_panc_ncell1000_nclust3)
tar_load(clustres_panc_ncell1000_nclust5)
tar_load(clustres_panc_ncell1000_nclust7)
tar_load(clustres_panc_ncell3000_nclust3)
tar_load(clustres_panc_ncell3000_nclust5)
tar_load(clustres_panc_ncell3000_nclust7)
tar_load(clustres_panc_ncell5000_nclust3)
tar_load(clustres_panc_ncell5000_nclust5)
tar_load(clustres_panc_ncell5000_nclust7)
tar_load(clustres_panc_ncell10000_nclust3)
tar_load(clustres_panc_ncell10000_nclust5)
tar_load(clustres_panc_ncell10000_nclust7)
# lung reference
tar_load(clustres_lung_ncell1000_nclust3)
tar_load(clustres_lung_ncell1000_nclust5)
tar_load(clustres_lung_ncell1000_nclust7)
tar_load(clustres_lung_ncell3000_nclust3)
tar_load(clustres_lung_ncell3000_nclust5)
tar_load(clustres_lung_ncell3000_nclust7)
tar_load(clustres_lung_ncell5000_nclust3)
tar_load(clustres_lung_ncell5000_nclust5)
tar_load(clustres_lung_ncell5000_nclust7)
tar_load(clustres_lung_ncell10000_nclust3)
tar_load(clustres_lung_ncell10000_nclust5)
tar_load(clustres_lung_ncell10000_nclust7)

Next we’ll coerce everything into one big dataframe. We’re not going to include the Leiden clustering results this time as they’re nearly identical to the Louvain results.

sim_results <- purrr::map(ls()[grepl("clustres", ls())], 
                          function(x) {
                            df <- eval(as.symbol(x))
                            df <- mutate(df, 
                                         reference = case_when(stringr::str_detect(x, "panc") ~ "Pancreas", 
                                                               TRUE ~ "Lung"), 
                                         dataset = x)
                            return(df)
                          }) %>% 
               purrr::reduce(rbind) %>% 
               filter(method != "Leiden (Seurat)") %>% 
               mutate(method = factor(method, 
                                      levels = c("K-means (Hartigan-Wong)", 
                                                 "Hierarchical (Ward)", 
                                                 "Louvain (Seurat)", 
                                                 "DBSCAN", 
                                                 "GiniClust3", 
                                                 "SCISSORS"), 
                                      labels = c("K-means", 
                                                 "Hierarchical", 
                                                 "Louvain (Seurat)", 
                                                 "DBSCAN", 
                                                 "GiniClust3", 
                                                 "SCISSORS")), 
                      runtime_minutes = case_when(runtime_units == "secs" ~ runtime / 60, 
                                                  runtime_units == "mins" ~ runtime, 
                                                  runtime_units == "hours" ~ runtime * 60, 
                                                  TRUE ~ NA_real_))

Analysis

Classification Error

First we’ll look at the distribution of ARI values achieved by each method over the entire parameter space. We note that this includes parameter values that are less than optimal for some methods, but this is somewhat realistic as it can be very difficult to obtain parameter optimality without ground truth labels for your data. In practice sub-optimal parameters are often chosen due to reliance on software defaults, researcher inexperience, or stochasticity in methods / analysis.

p0a <- ggplot(sim_results, aes(x = method, y = ari, fill = method)) + 
       facet_wrap(~reference) + 
       geom_violin(draw_quantiles = 0.5, 
                   color = "black", 
                   size = 1) + 
       ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                c("SCISSORS", "DBSCAN"), 
                                                c("SCISSORS", "GiniClust3"), 
                                                c("SCISSORS", "K-means"), 
                                                c("SCISSORS", "Hierarchical")), 
                             test = "wilcox.test", 
                             step_increase = 0.08,
                             map_signif_level = TRUE, 
                             vjust = 0.2,
                             textsize = 3) + 
       labs(fill = "Clustering Method", y = "Adjusted Rand Index") + 
       scale_y_continuous(labels = scales::percent_format(), breaks = seq(0, 1, by = 0.25)) + 
       scale_fill_paletteer_d("ggsci::nrc_npg") + 
       theme_classic(base_size = 14)  + 
       theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
             axis.title.x = element_blank(), 
             panel.grid.major.y = element_line())
p0a

We can make the same plot for overall performance, without splitting by reference dataset.

p0b <- ggplot(sim_results, aes(x = method, y = ari, fill = method)) + 
       geom_violin(draw_quantiles = 0.5, 
                   color = "black", 
                   size = 1) + 
       ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                c("SCISSORS", "DBSCAN"), 
                                                c("SCISSORS", "GiniClust3"), 
                                                c("SCISSORS", "K-means"), 
                                                c("SCISSORS", "Hierarchical")), 
                             test = "wilcox.test", 
                             step_increase = 0.08,
                             map_signif_level = TRUE, 
                             vjust = 0.2,
                             textsize = 3) + 
       labs(fill = "Clustering Method", y = "Adjusted Rand Index") + 
       scale_y_continuous(labels = scales::percent_format(), breaks = seq(0, 1, by = 0.25)) + 
       scale_fill_paletteer_d("ggsci::nrc_npg") + 
       theme_classic(base_size = 14)  + 
       theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
             axis.title.x = element_blank(), 
             panel.grid.major.y = element_line())
p0b

We’ll repeat the plot for the Normalized Mutual Information.

p1a <- ggplot(sim_results, aes(x = method, y = nmi, fill = method)) + 
       facet_wrap(~reference) + 
       geom_violin(draw_quantiles = 0.5, 
                   color = "black", 
                   size = 1) + 
       ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                 c("SCISSORS", "DBSCAN"), 
                                                 c("SCISSORS", "GiniClust3"), 
                                                 c("SCISSORS", "K-means"), 
                                                 c("SCISSORS", "Hierarchical")), 
                             test = "wilcox.test", 
                             step_increase = 0.08,
                             map_signif_level = TRUE, 
                             vjust = 0.2,
                             textsize = 3) + 
       labs(fill = "Clustering Method", y = "Normalied Mutual Information") + 
       scale_y_continuous(labels = scales::percent_format(), breaks = seq(0, 1, by = 0.25)) + 
       scale_fill_paletteer_d("ggsci::nrc_npg") + 
       theme_classic(base_size = 14)  + 
       theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
             axis.title.x = element_blank(), 
             panel.grid.major.y = element_line())
p1a

Again, the entire dataset without splitting by reference.

p1b <- ggplot(sim_results, aes(x = method, y = nmi, fill = method)) + 
       geom_violin(draw_quantiles = 0.5, 
                   color = "black", 
                   size = 1) + 
       ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                 c("SCISSORS", "DBSCAN"), 
                                                 c("SCISSORS", "GiniClust3"), 
                                                 c("SCISSORS", "K-means"), 
                                                 c("SCISSORS", "Hierarchical")), 
                             test = "wilcox.test", 
                             step_increase = 0.08,
                             map_signif_level = TRUE, 
                             vjust = 0.2,
                             textsize = 3) + 
       labs(fill = "Clustering Method", y = "Normalied Mutual Information") + 
       scale_y_continuous(labels = scales::percent_format(), breaks = seq(0, 1, by = 0.25)) + 
       scale_fill_paletteer_d("ggsci::nrc_npg") + 
       theme_classic(base_size = 14)  + 
       theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
             axis.title.x = element_blank(), 
             panel.grid.major.y = element_line())
p1b

And lastly for the mean silhouette score.

p2a <- ggplot(sim_results, aes(x = method, y = sil, fill = method)) + 
       facet_wrap(~reference) + 
       geom_violin(draw_quantiles = 0.5, 
                   color = "black", 
                   size = 1) + 
       ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                c("SCISSORS", "DBSCAN"), 
                                                c("SCISSORS", "GiniClust3"), 
                                                c("SCISSORS", "K-means"), 
                                                c("SCISSORS", "Hierarchical")), 
                             test = "wilcox.test", 
                             step_increase = 0.08,
                             map_signif_level = TRUE, 
                             vjust = 0.2,
                             textsize = 3) + 
       labs(fill = "Clustering Method", y = "Mean Silhouette Score") + 
       scale_y_continuous(labels = scales::number_format(accuracy = .1), 
                          breaks = seq(round(min(sim_results$sil)), 1, by = 0.25)) + 
       scale_fill_paletteer_d("ggsci::nrc_npg") + 
       theme_classic(base_size = 14)  + 
       theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
             axis.title.x = element_blank(), 
             panel.grid.major.y = element_line())
p2a

Without splitting by reference:

p2b <- ggplot(sim_results, aes(x = method, y = sil, fill = method)) + 
       geom_violin(draw_quantiles = 0.5, 
                   color = "black", 
                   size = 1) + 
       ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                c("SCISSORS", "DBSCAN"), 
                                                c("SCISSORS", "GiniClust3"), 
                                                c("SCISSORS", "K-means"), 
                                                c("SCISSORS", "Hierarchical")), 
                             test = "wilcox.test", 
                             step_increase = 0.08,
                             map_signif_level = TRUE, 
                             vjust = 0.2,
                             textsize = 3) + 
       labs(fill = "Clustering Method", y = "Mean Silhouette Score") + 
       scale_y_continuous(labels = scales::number_format(accuracy = .1), 
                          breaks = seq(round(min(sim_results$sil)), 1, by = 0.25)) + 
       scale_fill_paletteer_d("ggsci::nrc_npg") + 
       theme_classic(base_size = 14)  + 
       theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
             axis.title.x = element_blank(), 
             panel.grid.major.y = element_line())
p2b

Next we’ll subset the data to only include ARI values above the median for each method, which we are doing in order to mimic reasonable parameter selection by experienced researchers.

p3 <- sim_results %>% 
      with_groups(c(reference, method), 
                  filter, 
                  ari > median(ari)) %>% 
      ggplot(aes(x = method, y = ari, fill = method)) + 
      facet_wrap(~reference) + 
      geom_violin(draw_quantiles = 0.5, 
                  color = "black", 
                  size = 1) + 
      ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                c("SCISSORS", "DBSCAN"), 
                                                c("SCISSORS", "GiniClust3"), 
                                                c("SCISSORS", "K-means"), 
                                                c("SCISSORS", "Hierarchical")), 
                            test = "wilcox.test",
                            step_increase = 0.08,
                            map_signif_level = TRUE, 
                            vjust = 0.2,
                            textsize = 3) + 
      labs(fill = "Clustering Method",
           y = "Adjusted Rand Index", 
           caption = "Top 50% of clustering runs") + 
      scale_y_continuous(labels = scales::percent_format(), breaks = seq(0, 1, by = 0.25)) + 
      scale_fill_paletteer_d("ggsci::nrc_npg") + 
      theme_classic(base_size = 14)  + 
      theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
            axis.title.x = element_blank(), 
            panel.grid.major.y = element_line(), 
            plot.caption = element_text(face = "italic"))
p3

We’ll repeat the filtered plot for the NMI of each method.

p4 <- sim_results %>% 
      with_groups(c(reference, method), 
                  filter, 
                  nmi > median(nmi)) %>% 
      ggplot(aes(x = method, y = nmi, fill = method)) + 
      facet_wrap(~reference) + 
      geom_violin(draw_quantiles = 0.5, 
                  color = "black", 
                  size = 1) + 
      ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                c("SCISSORS", "DBSCAN"), 
                                                c("SCISSORS", "GiniClust3"), 
                                                c("SCISSORS", "K-means"), 
                                                c("SCISSORS", "Hierarchical")), 
                            test = "wilcox.test",
                            step_increase = 0.08,
                            map_signif_level = TRUE, 
                            vjust = 0.2,
                            textsize = 3) + 
      labs(fill = "Clustering Method", 
           y = "Normalized Mutual Information", 
           caption = "Top 50% of clustering runs") + 
      scale_y_continuous(labels = scales::percent_format(), breaks = seq(0, 1, by = 0.25)) + 
      scale_fill_paletteer_d("ggsci::nrc_npg") + 
      theme_classic(base_size = 14)  + 
      theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
            axis.title.x = element_blank(), 
            panel.grid.major.y = element_line(), 
            plot.caption = element_text(face = "italic"))
p4

As well as for the silhouette score.

p5 <- sim_results %>% 
      with_groups(c(reference, method), 
                  filter, 
                  sil > median(sil)) %>% 
      ggplot(aes(x = method, y = sil, fill = method)) + 
      facet_wrap(~reference) + 
      geom_violin(draw_quantiles = 0.5,
                  color = "black", 
                  size = 1) + 
      ggsignif::geom_signif(comparisons = list(c("SCISSORS", "Louvain (Seurat)"), 
                                                c("SCISSORS", "DBSCAN"), 
                                                c("SCISSORS", "GiniClust3"), 
                                                c("SCISSORS", "K-means"), 
                                                c("SCISSORS", "Hierarchical")), 
                            test = "wilcox.test",
                            step_increase = 0.08,
                            map_signif_level = TRUE, 
                            vjust = 0.2,
                            textsize = 3) + 
      labs(fill = "Clustering Method", 
           y = "Silhouette Score", 
           caption = "Top 50% of clustering runs") + 
      scale_y_continuous(labels = scales::number_format(accuracy = .1), 
                          breaks = seq(round(min(sim_results$sil)), 1, by = 0.25)) + 
      scale_fill_paletteer_d("ggsci::nrc_npg") + 
      theme_classic(base_size = 14)  + 
      theme(axis.text.x = element_text(angle = 45, hjust = 0.9, vjust = 0.9), 
            axis.title.x = element_blank(), 
            panel.grid.major.y = element_line(), 
            plot.caption = element_text(face = "italic"))
p5

Simulation Case Study

We can also plot an example of how the various methods categorize an example dataset. We’ll need to run each method individually, using the same code that we did in our {targets} pipeline. First we’ll load in a simulated dataset from the pancreas reference with 5,000 cells and 7 clusters.

tar_load(sim_panc_ncell1000_nclust7)

Here’s what the ground truth labels from our simulations look like. Some of the clusters are easily separable, and some others are less so.

sim_panc_ncell1000_nclust7@meta.data$cellPopulation2 <- as.integer(sim_panc_ncell1000_nclust7@meta.data$cellPopulation) - 1L
p6 <- DimPlot(sim_panc_ncell1000_nclust7, reduction = "tsne", group.by = "cellPopulation2") + 
      scale_color_paletteer_d("ggsci::category10_d3") + 
      labs(x = "t-SNE 1", 
           y = "t-SNE 2",  
           color = "True\nLabel") + 
      theme_classic(base_size = 14) + 
      theme(axis.ticks = element_blank(), 
            axis.text = element_blank(), 
            plot.title = element_blank(), 
            legend.title = element_text(face = "bold")) + 
      guides(color = guide_legend(override.aes = list(size = 3)))
p6

Here we run the {SCISSORS} reclustering over all clusters, optimizing for maximum silhouette score. First, we estimate an initial broad clustering, which we can compare to the true labels below.

seu <- Seurat::FindClusters(sim_panc_ncell1000_nclust7, 
                            resolution = 0.1, 
                            algorithm = 1, 
                            random.seed = 312, 
                            verbose = FALSE)

Now we can recluster. We’ll use the default value of the mean silhouette cutoff score, 0.25. For other parameters, we use the same sets of possible values that we used in evaluating performance on all our simulated datasets. For the number of principal components, we’ll use 20 as there’s not too many cells in this dataset.

SCISSORS_clusts <- SCISSORS::ReclusterCells(seurat.object = seu, 
                                            auto = TRUE, 
                                            use.parallel = FALSE, 
                                            resolution.vals = seq(0.1, 0.7, by = c(0.1)), 
                                            k.vals = c(10, 25, 40, 60), 
                                            merge.clusters = FALSE, 
                                            use.sct = FALSE, 
                                            n.HVG = 2000, 
                                            n.PC = 20, 
                                            cutoff.score = 0.25, 
                                            random.seed = 312)
## [1] "Choosing reclustering candidates automatically."
## [1] "Reclustering cells in cluster 5 using k = 40 & resolution = 0.4; S = 0.296"
seu_new <- SCISSORS::IntegrateSubclusters(original.object = seu, reclust.results = SCISSORS_clusts)
SCISSORS_clusts <- as.integer(seu_new$seurat_clusters) - 1L

Next we run Louvain clustering with a reasonable resolution value of 0.5. We’ll make sure to also use 20 PCs in the creation of the SNN graph here.

Louvain_clusts <- Seurat::FindNeighbors(sim_panc_ncell1000_nclust7, 
                                        reduction = "pca", 
                                        dims = 1:20, 
                                        nn.method = "annoy", 
                                        annoy.metric = "cosine", 
                                        verbose = FALSE) %>% 
                  Seurat::FindClusters(resolution = 0.5, 
                                       algorithm = 1, 
                                       random.seed = 312, 
                                       verbose = FALSE)
Louvain_clusts <- as.integer(Louvain_clusts$seurat_clusters) - 1L

We’ll run Leiden as well, with the same resolution.

reticulate::use_virtualenv("/nas/longleaf/home/jrleary/Python", required = TRUE)
Leiden_clusts <- Seurat::FindClusters(sim_panc_ncell1000_nclust7, 
                                      resolution = 0.5, 
                                      algorithm = 4, 
                                      random.seed = 312, 
                                      verbose = FALSE)$seurat_clusters 
Leiden_clusts <- as.integer(Leiden_clusts) - 1L

For \(k\)-means, we’ll use the true value \(k = 7\), even though in practice we won’t know that parameter.

kmeans_clusts <- kmeans(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20],
                        centers = 7,
                        nstart = 5,
                        algorithm = "Hartigan-Wong")$cluster 
kmeans_clusts <- as.integer(kmeans_clusts) - 1L

We’ll do the same with hierarchical clustering.

hclust_tree <- hclust(SCISSORS::CosineDist(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20]), method = "ward.D2")
hclust_clusts <- as.integer(cutree(hclust_tree, k = 7)) - 1L

Lastly, for DBSCAN ’ll first choose a reasonable value for the epsilon parameter by looking for the inflection point in the KNN distance plot. In our simulations we did this automatically by running several segmented regressions for changepoint detection and iterating over those values, but in this case we’ll choose one manually. It looks like \(\epsilon = 8\) is a reasonable value.

dbscan::kNNdistplot(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20], k = 10)
abline(h = 8, col = "firebrick", lty = "dashed")

dbscan_clusts <- dbscan::dbscan(scale(sim_panc_ncell1000_nclust7@reductions$pca@cell.embeddings[, 1:20], scale = FALSE), 
                                eps = 8,  
                                minPts = 5, 
                                borderPoints = TRUE)$cluster

Lastly we’ll run GiniClust3 using the default parameters. We’ll need to switch to Python to use this method, but first we’ll need to make sure we can read the counts matrix from R to Python.

count_mat <- as.matrix(sim_panc_ncell1000_nclust7@assays$RNA@counts)

Now we can run the algorithm. We’ll use the default values for parameters of interest as found in the {GiniClust3} docs.

# import libraries 
import anndata           # annotated single cell data
import numpy as np       # matrix algebra
import pandas as pd      # DataFrames
import scanpy as sc      # ScanPy
import giniclust3 as gc  # GiniClust3
# create AnnData object
adata = anndata.AnnData(X=r.count_mat.T)
sc.pp.normalize_per_cell(adata, counts_per_cell_after=1e4)
# calculate clustering 
gc.gini.calGini(adata, selection='p_value', p_value=0.001)
## Gene number is 15270
## Cell number is 996
gc.fano.calFano(adata, method='scanpy')
adataGini = gc.gini.clusterGini(adata, resolution=0.1, neighbors=5)
adataFano = gc.fano.clusterFano(adata, resolution=0.1, neighbors=15)
consensusCluster = {}
consensusCluster['giniCluster'] = np.array(adata.obs['rare'].values.tolist())
consensusCluster['fanoCluster'] = np.array(adata.obs['fano'].values.tolist())
gc.consensus.generateMtilde(consensusCluster)
gc.consensus.clusterMtilde(consensusCluster)

We now bring the clustering results back into R.

giniclust3_clusts <- as.integer(py$consensusCluster$finalCluster)

Now let’s bring all our value together & plot them.

clust_comp <- data.frame(SCISSORS = SCISSORS_clusts, 
                         Louvain = Louvain_clusts, 
                         Leiden = Leiden_clusts, 
                         Kmeans = kmeans_clusts, 
                         Hierarchical = hclust_clusts, 
                         DBSCAN = dbscan_clusts, 
                         GiniClust3 = giniclust3_clusts, 
                         tSNE_1 = Embeddings(sim_panc_ncell1000_nclust7, "tsne")[, 1], 
                         tSNE_2 = Embeddings(sim_panc_ncell1000_nclust7, "tsne")[, 2]) %>% 
              tidyr::pivot_longer(!contains("tSNE"), names_to = "method", values_to = "cluster") %>% 
              mutate(across(!contains("tSNE"), as.factor)) %>% 
              filter(method != "Leiden")
p7 <- ggplot(clust_comp, aes(x = tSNE_1, y = tSNE_2, color = cluster)) + 
      facet_wrap(~method) + 
      geom_point(size = 0.75) + 
      scale_color_paletteer_d("ggsci::category10_d3") + 
      labs(x = "t-SNE 1", 
           y = "t-SNE 2",  
           color = "Estimated\nCluster") + 
      theme_classic(base_size = 14) + 
      theme(axis.ticks = element_blank(), 
            axis.text = element_blank(), 
            plot.title = element_blank(), 
            legend.title = element_text(face = "bold")) + 
      guides(color = guide_legend(override.aes = list(size = 3)))
p7

We’ll also put together sub-tables of the ARI values & mean silhouette scores for each method.

ari_res <- data.frame(ARI = c(mclust::adjustedRandIndex(sim_panc_ncell1000_nclust7$cellPopulation, SCISSORS_clusts), 
                              mclust::adjustedRandIndex(sim_panc_ncell1000_nclust7$cellPopulation, Louvain_clusts), 
                              mclust::adjustedRandIndex(sim_panc_ncell1000_nclust7$cellPopulation, kmeans_clusts), 
                              mclust::adjustedRandIndex(sim_panc_ncell1000_nclust7$cellPopulation, hclust_clusts), 
                              mclust::adjustedRandIndex(sim_panc_ncell1000_nclust7$cellPopulation, dbscan_clusts), 
                              mclust::adjustedRandIndex(sim_panc_ncell1000_nclust7$cellPopulation, giniclust3_clusts)), 
                      Method = c("SCISSORS", "Louvain", "K-means", "Hierarchical", "DBSCAN", "GiniClust3")) %>% 
           arrange(desc(ARI)) %>% 
           mutate(ARI_Rank = row_number()) %>% 
           select(Method, ARI, ARI_Rank)
sil_res <- data.frame(Sil = c(mean(cluster::silhouette(SCISSORS::CosineDist(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20]), x = SCISSORS_clusts)[, 3]), 
                              mean(cluster::silhouette(SCISSORS::CosineDist(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20]), x = Louvain_clusts)[, 3]), 
                              mean(cluster::silhouette(SCISSORS::CosineDist(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20]), x = kmeans_clusts)[, 3]), 
                              mean(cluster::silhouette(SCISSORS::CosineDist(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20]), x = hclust_clusts)[, 3]), 
                              mean(cluster::silhouette(SCISSORS::CosineDist(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20]), x = dbscan_clusts)[, 3]), 
                              mean(cluster::silhouette(SCISSORS::CosineDist(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20]), x = giniclust3_clusts)[, 3])), 
                      Method = c("SCISSORS", "Louvain", "K-means", "Hierarchical", "DBSCAN", "GiniClust3")) %>% 
           arrange(desc(Sil)) %>% 
           mutate(Sil_Rank = row_number()) %>% 
           select(Method, Sil, Sil_Rank)
ari_table <- gridExtra::tableGrob(ari_res,
                                  cols = c("Method", "Adj. Rand Index", "Rank"), 
                                  theme = gridExtra::ttheme_minimal(colhead = list(bg_params = list(fill = "grey90"))), 
                                  rows = NULL)
sil_table <- gridExtra::tableGrob(sil_res,
                                  cols = c("Method", "Mean Silhouette Score", "Rank"), 
                                  theme = gridExtra::ttheme_minimal(colhead = list(bg_params = list(fill = "grey90"))), 
                                  rows = NULL)
metric_res <- ari_res %>% 
              inner_join(sil_res, by = "Method") %>% 
              mutate(Combined_Rank = ARI_Rank + Sil_Rank) %>% 
              arrange(Combined_Rank) %>% 
              mutate(Rank = row_number()) %>% 
              select(Method, 
                     ARI, 
                     ARI_Rank, 
                     Sil, 
                     Sil_Rank, 
                     Rank)
metric_table <- gridExtra::tableGrob(metric_res,
                                     cols = c("Method", "ARI", "ARI Rank", "Silhouette", "Silhouette Rank", "Overall Rank"), 
                                     theme = gridExtra::ttheme_minimal(colhead = list(bg_params = list(fill = "grey90"))), 
                                     rows = NULL)

Looking at the results, we see that {SCISSORS} almost exactly recaptures the original clustering, and also provides the best measure of cluster fit (silhouette score). Even methods that know the true number of cluster a priori, \(k\)-means and hierarchical clustering, ignore the two small subclusters in favor of splitting up larger clusters.

p8a <- (p7 / (((p6 + facet_wrap(~"Ground Truth")) | metric_table))) + 
       plot_layout(heights = c(2, 1)) 
p8a

Lastly, we’ll show that simply naively increasing the resolution parameter of the Louvain clustering does not lead to better results.

Louvain_res_incr <- purrr::map(c(.5, .7, .9, 1.1, 1.5, 2), function(x) {
  cluster_res <- Seurat::FindClusters(sim_panc_ncell1000_nclust7, 
                                      resolution = x, 
                                      algorithm = 1, 
                                      random.seed = 312, 
                                      verbose = FALSE)
  cluster_vals <- as.integer(cluster_res$seurat_clusters) - 1L
  ari_val <- mclust::adjustedRandIndex(cluster_res$seurat_clusters, sim_panc_ncell1000_nclust7$cellPopulation)
  sil_val <- mean(cluster::silhouette(SCISSORS::CosineDist(Embeddings(sim_panc_ncell1000_nclust7, "pca")[, 1:20]), 
                                      x = as.integer(cluster_res$seurat_clusters) - 1L)[, 3])
  return(list(Clustering = cluster_vals, 
              ARI = ari_val, 
              Silhouette = sil_val))
})
res_df <- data.frame(resolution = rep(c(.5, .7, .9, 1.1, 1.5, 2), each = ncol(sim_panc_ncell1000_nclust7)), 
                     clust = unlist(purrr::map(Louvain_res_incr, \(x) x$Clustering)), 
                     tSNE_1 = rep(Embeddings(sim_panc_ncell1000_nclust7, "tsne")[, 1], 6), 
                     tSNE_2 = rep(Embeddings(sim_panc_ncell1000_nclust7, "tsne")[, 2], 6)) %>% 
          mutate(resolution = factor(resolution, labels = c("0.5", "0.7", "0.9", "1.1", "1.5", "2.0")), 
                 clust = as.factor(clust))
louvain_metric_res <- data.frame(Res = factor(c(.5, .7, .9, 1.1, 1.5, 2.0), 
                                              labels = c("0.5", "0.7", "0.9", "1.1", "1.5", "2.0")), 
                                 ARI = purrr::map_dbl(Louvain_res_incr, \(x) x$ARI), 
                                 Sil = purrr::map_dbl(Louvain_res_incr, \(x) x$ARI)) %>% 
                      arrange(desc(ARI))
louvain_ari_table <- gridExtra::tableGrob(louvain_metric_res,
                                          cols = c("Resolution", "Adj. Rand Index", "Silhouette"), 
                                          theme = gridExtra::ttheme_minimal(colhead = list(bg_params = list(fill = "grey90"))), 
                                          rows = NULL)

We see that the Louvain algorithm tends to split off larger clusters first before identifying the true split between the two smaller clusters (true clusters 5 & 6).

p8b <- ggplot(res_df, aes(x = tSNE_1, y = tSNE_2, color = clust)) + 
       facet_wrap(~paste0("Resolution: ", resolution)) + 
       geom_point(size = 0.75) + 
       scale_color_paletteer_d("ggsci::category10_d3") + 
       labs(x = "t-SNE 1", 
            y = "t-SNE 2",  
            color = "Louvain\nCluster") + 
       theme_classic(base_size = 14) + 
       theme(axis.ticks = element_blank(), 
             axis.text = element_blank(), 
             plot.title = element_blank(), 
             legend.title = element_text(face = "bold")) + 
       guides(color = guide_legend(override.aes = list(size = 3)))
(p8b / (p6 | louvain_ari_table)) + plot_layout(heights = c(2, 1))      

Computational Cost

Next, let’s take a look at how the runtimes of the various methods compare. {SCISSORS} has the highest runtime, as expected because it performs multiple runs of the Louvain algorithm, as opposed to most of the simpler methods which are single-pass.

p9a <- sim_results %>% 
       mutate(dataset_size = stringr::str_extract(dataset, "ncell.*_"), 
              dataset_size = stringr::str_replace(stringr::str_replace(dataset_size, "ncell", ""), "_", ""), 
              dataset_size = as.integer(dataset_size), 
              dataset_size = factor(dataset_size, levels = c("1000", "3000", "5000", "10000"))) %>% 
       ggplot(aes(x = method, y = runtime_minutes, fill = method)) + 
       facet_wrap(~method, scales = "free_x") + 
       geom_violin(draw_quantiles = 0.5, 
                   scale = "width", 
                   size = 1, 
                   color = "black") + 
       scale_y_continuous(trans = "log", labels = scales::number_format(accuracy = .1)) + 
       scale_fill_paletteer_d("ggsci::nrc_npg") + 
       labs(y = "log(Runtime)", fill = "Clustering Method") + 
       theme_classic(base_size = 14) + 
       theme(axis.ticks.x = element_blank(), 
             panel.grid.major.y = element_line(), 
             axis.title.x = element_blank())
p9a

We’ll also plot un-transformed runtime, though the plot is harder to read because of the disparity in runtimes between simpler and more complex methods. It’s expected that {SCISSORS} have longer runtimes as it’s a multi-pass algorithm, but even for larger datasets it rarely exceeds 10 minutes of runtime thanks to automatic identification of reclustering targets.

p9b <- sim_results %>% 
       mutate(dataset_size = stringr::str_extract(dataset, "ncell.*_"), 
              dataset_size = stringr::str_replace(stringr::str_replace(dataset_size, "ncell", ""), "_", ""), 
              dataset_size = as.integer(dataset_size), 
              dataset_size = factor(dataset_size, 
                                    levels = c(1000L, 3000L, 5000L, 10000L), 
                                    labels = c("1,000", "3,000", "5,000", "10,000"))) %>% 
       ggplot(aes(x = dataset_size, y = runtime_minutes, fill = method, group = dataset_size)) + 
       facet_wrap(~method, scales = "free") + 
       geom_violin(draw_quantiles = 0.5, 
                   position = position_dodge(1), 
                   scale = "width", 
                   size = 1, 
                   color = "black") + 
       scale_y_continuous(labels = scales::number_format(accuracy = .01)) + 
       scale_fill_paletteer_d("ggsci::nrc_npg") + 
       labs(x = "Number of Cells", 
            y = "Runtime (minutes)", 
            fill = "Clustering Method") + 
       theme_classic(base_size = 14) + 
       theme(axis.ticks.x = element_blank(), 
             panel.grid.major.y = element_line())
p9b

Save Figures

First we’ll define a convenience function to help save our plots.

fig_save <- function(plot.obj, plot.name = "", dims = c(9, 5)) {
  ggplot2::ggsave(filename = plot.name, 
                  path = "./figures", 
                  plot = plot.obj, 
                  device = "pdf", 
                  width = dims[1], 
                  height = dims[2], 
                  units = "in", 
                  dpi = "retina")
}

Now let’s save them all as PDFs.

fig_save(p0a, plot.name = "Total_ARI_By_Method_And_Reference.pdf", dims = c(10, 6))
fig_save(p0b, plot.name = "Total_ARI_By_Method.pdf", dims = c(10, 6))
fig_save(p1a, plot.name = "Total_NMI_By_Method_And_Reference.pdf", dims = c(10, 6))
fig_save(p1b, plot.name = "Total_NMI_By_Method.pdf", dims = c(10, 6))
fig_save(p2a, plot.name = "Total_Silhouette_By_Method_And_Reference.pdf", dims = c(10, 6))
fig_save(p2b, plot.name = "Total_Silhouette_By_Method.pdf", dims = c(10, 6))
fig_save(p3, plot.name = "Above_Median_ARI_By_Method_And_Reference.pdf", dims = c(10, 6))
fig_save(p4, plot.name = "Above_Median_NMI_By_Method_And_Reference.pdf", dims = c(10, 6))
fig_save(p5, plot.name = "Above_Median_Silhouette_By_Method_And_Reference.pdf", dims = c(10, 6))
fig_save(p6, plot.name = "Case_Study_True_Label_tSNE.pdf", dims = c(8, 4.5))
fig_save(p7, plot.name = "Case_Study_Estimated_Clusters_All_tSNE.pdf", dims = c(12, 7))
fig_save(p8a, plot.name = "Case_Study_Estimated_Clusters_Performance.pdf", dims = c(15, 8))
fig_save(p8b, plot.name = "Case_Study_Louvain_Resolution_Performance.pdf", dims = c(15, 8))
fig_save(p9a, plot.name = "Runtime__Log_By_Method_And_Reference.pdf", dims = c(12, 7))
fig_save(p9b, plot.name = "Runtime_Raw_By_Method_And_Number_Cells.pdf", dims = c(12, 7))

Session Info

sessioninfo::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.1.3 (2022-03-10)
##  os       Red Hat Enterprise Linux 8.5 (Ootpa)
##  system   x86_64, linux-gnu
##  ui       X11
##  language (EN)
##  collate  en_US.UTF-8
##  ctype    en_US.UTF-8
##  tz       America/New_York
##  date     2022-11-13
##  pandoc   2.11.4 @ /nas/longleaf/apps/rstudio-server/1.4.1717/bin/pandoc/ (via rmarkdown)
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package              * version   date (UTC) lib source
##  abind                  1.4-5     2016-07-21 [2] CRAN (R 4.1.3)
##  AnnotationDbi          1.56.2    2021-11-09 [2] Bioconductor
##  aricode              * 1.0.1     2022-09-05 [1] CRAN (R 4.1.3)
##  assertthat             0.2.1     2019-03-21 [2] CRAN (R 4.1.3)
##  backports              1.4.1     2021-12-13 [1] CRAN (R 4.1.3)
##  base64url              1.4       2018-05-14 [1] CRAN (R 4.1.3)
##  Biobase              * 2.54.0    2021-10-26 [2] Bioconductor
##  BiocFileCache          2.2.1     2022-01-23 [2] Bioconductor
##  BiocGenerics         * 0.40.0    2021-10-26 [2] Bioconductor
##  biomaRt                2.50.3    2022-02-03 [2] Bioconductor
##  Biostrings             2.62.0    2021-10-26 [2] Bioconductor
##  bit                    4.0.4     2020-08-04 [2] CRAN (R 4.1.3)
##  bit64                  4.0.5     2020-08-30 [2] CRAN (R 4.1.3)
##  bitops                 1.0-7     2021-04-24 [2] CRAN (R 4.1.3)
##  blob                   1.2.3     2022-04-10 [1] CRAN (R 4.1.3)
##  broom                  1.0.0     2022-07-01 [1] CRAN (R 4.1.3)
##  bslib                  0.4.0     2022-07-16 [1] CRAN (R 4.1.3)
##  cachem                 1.0.6     2021-08-19 [1] CRAN (R 4.1.0)
##  callr                  3.7.2     2022-08-22 [2] CRAN (R 4.1.3)
##  cellranger             1.1.0     2016-07-27 [2] CRAN (R 4.1.3)
##  cli                    3.3.0     2022-04-25 [1] CRAN (R 4.1.3)
##  cluster              * 2.1.3     2022-03-28 [1] CRAN (R 4.1.0)
##  codetools              0.2-18    2020-11-04 [2] CRAN (R 4.1.3)
##  colorspace             2.0-3     2022-02-21 [1] CRAN (R 4.1.0)
##  cowplot                1.1.1     2020-12-30 [2] CRAN (R 4.1.3)
##  crayon                 1.5.1     2022-03-26 [1] CRAN (R 4.1.3)
##  curl                   4.3.2     2021-06-23 [2] CRAN (R 4.1.3)
##  data.table             1.14.2    2021-09-27 [1] CRAN (R 4.1.0)
##  DBI                    1.1.3     2022-06-18 [1] CRAN (R 4.1.0)
##  dbplyr                 2.2.1     2022-06-27 [1] CRAN (R 4.1.3)
##  dbscan               * 1.1-11    2022-10-27 [1] CRAN (R 4.1.3)
##  DelayedArray           0.20.0    2021-10-26 [2] Bioconductor
##  deldir                 1.0-6     2021-10-23 [1] CRAN (R 4.1.3)
##  digest                 0.6.29    2021-12-01 [1] CRAN (R 4.1.0)
##  doParallel             1.0.17    2022-02-07 [1] CRAN (R 4.1.3)
##  dplyr                * 1.0.9     2022-04-28 [1] CRAN (R 4.1.3)
##  ellipsis               0.3.2     2021-04-29 [2] CRAN (R 4.1.3)
##  evaluate               0.15      2022-02-18 [1] CRAN (R 4.1.0)
##  fansi                  1.0.3     2022-03-24 [1] CRAN (R 4.1.0)
##  farver                 2.1.1     2022-07-06 [2] CRAN (R 4.1.3)
##  fastmap                1.1.0     2021-01-25 [2] CRAN (R 4.1.3)
##  filelock               1.0.2     2018-10-05 [2] CRAN (R 4.1.3)
##  fitdistrplus           1.1-8     2022-03-10 [1] CRAN (R 4.1.3)
##  forcats              * 0.5.2     2022-08-19 [2] CRAN (R 4.1.3)
##  foreach                1.5.2     2022-02-02 [1] CRAN (R 4.1.3)
##  fs                     1.5.2     2021-12-08 [1] CRAN (R 4.1.0)
##  future               * 1.27.0    2022-07-22 [1] CRAN (R 4.1.3)
##  future.apply           1.9.0     2022-04-25 [1] CRAN (R 4.1.3)
##  future.callr         * 0.8.0     2022-04-01 [1] CRAN (R 4.1.3)
##  gargle                 1.2.1     2022-09-08 [2] CRAN (R 4.1.3)
##  generics               0.1.3     2022-07-05 [1] CRAN (R 4.1.3)
##  GenomeInfoDb         * 1.28.4    2021-09-05 [1] Bioconductor
##  GenomeInfoDbData       1.2.7     2022-03-31 [2] Bioconductor
##  GenomicRanges        * 1.46.1    2021-11-18 [2] Bioconductor
##  ggplot2              * 3.3.6     2022-05-03 [1] CRAN (R 4.1.3)
##  ggrepel                0.9.1     2021-01-15 [1] CRAN (R 4.1.0)
##  ggridges               0.5.3     2021-01-08 [2] CRAN (R 4.1.3)
##  ggsignif               0.6.3     2021-09-09 [1] CRAN (R 4.1.0)
##  globals                0.15.1    2022-06-24 [1] CRAN (R 4.1.3)
##  glue                   1.6.0     2021-12-17 [1] CRAN (R 4.1.0)
##  goftest                1.2-3     2021-10-07 [1] CRAN (R 4.1.0)
##  googledrive            2.0.0     2021-07-08 [2] CRAN (R 4.1.3)
##  googlesheets4          1.0.1     2022-08-13 [2] CRAN (R 4.1.3)
##  gridExtra              2.3       2017-09-09 [2] CRAN (R 4.1.3)
##  gtable                 0.3.1     2022-09-01 [2] CRAN (R 4.1.3)
##  haven                  2.5.0     2022-04-15 [1] CRAN (R 4.1.3)
##  here                   1.0.1     2020-12-13 [1] CRAN (R 4.1.0)
##  highr                  0.9       2021-04-16 [2] CRAN (R 4.1.3)
##  hms                    1.1.1     2021-09-26 [1] CRAN (R 4.1.0)
##  htmltools              0.5.3     2022-07-18 [1] CRAN (R 4.1.3)
##  htmlwidgets            1.5.4     2021-09-08 [1] CRAN (R 4.1.0)
##  httpuv                 1.6.5     2022-01-05 [1] CRAN (R 4.1.3)
##  httr                   1.4.3     2022-05-04 [1] CRAN (R 4.1.3)
##  ica                    1.0-3     2022-07-08 [2] CRAN (R 4.1.3)
##  igraph                 1.3.4     2022-07-19 [1] CRAN (R 4.1.3)
##  iotools                0.3-2     2021-07-23 [1] CRAN (R 4.1.3)
##  IRanges              * 2.28.0    2021-10-26 [2] Bioconductor
##  irlba                  2.3.5     2021-12-06 [1] CRAN (R 4.1.3)
##  iterators              1.0.14    2022-02-05 [1] CRAN (R 4.1.0)
##  jquerylib              0.1.4     2021-04-26 [2] CRAN (R 4.1.3)
##  jsonlite               1.8.0     2022-02-22 [1] CRAN (R 4.1.0)
##  KEGGREST               1.34.0    2021-10-26 [2] Bioconductor
##  KernSmooth             2.23-20   2021-05-03 [2] CRAN (R 4.1.3)
##  knitr                  1.39      2022-04-26 [1] CRAN (R 4.1.3)
##  labeling               0.4.2     2020-10-20 [2] CRAN (R 4.1.3)
##  later                  1.3.0     2021-08-18 [1] CRAN (R 4.1.0)
##  lattice                0.20-45   2021-09-22 [2] CRAN (R 4.1.3)
##  lazyeval               0.2.2     2019-03-15 [2] CRAN (R 4.1.3)
##  leiden                 0.4.2     2022-05-09 [1] CRAN (R 4.1.3)
##  lifecycle              1.0.1     2021-09-24 [1] CRAN (R 4.1.0)
##  listenv                0.8.0     2019-12-05 [2] CRAN (R 4.1.3)
##  lmtest                 0.9-40    2022-03-21 [1] CRAN (R 4.1.3)
##  logging                0.10-108  2019-07-14 [1] CRAN (R 4.1.3)
##  lubridate              1.8.0     2021-10-07 [1] CRAN (R 4.1.0)
##  magrittr             * 2.0.3     2022-03-30 [1] CRAN (R 4.1.0)
##  MASS                 * 7.3-58    2022-07-14 [1] CRAN (R 4.1.3)
##  Matrix                 1.5-1     2022-09-13 [1] CRAN (R 4.1.3)
##  MatrixGenerics       * 1.4.3     2021-08-26 [1] Bioconductor
##  matrixStats          * 0.62.0    2022-04-19 [1] CRAN (R 4.1.3)
##  mclust               * 5.4.10    2022-05-20 [1] CRAN (R 4.1.0)
##  memoise                2.0.1     2021-11-26 [2] CRAN (R 4.1.3)
##  mgcv                   1.8-40    2022-03-29 [1] CRAN (R 4.1.0)
##  mime                   0.12      2021-09-28 [1] CRAN (R 4.1.0)
##  miniUI                 0.1.1.1   2018-05-18 [2] CRAN (R 4.1.3)
##  modelr                 0.1.9     2022-08-19 [2] CRAN (R 4.1.3)
##  munsell                0.5.0     2018-06-12 [2] CRAN (R 4.1.3)
##  nlme                 * 3.1-158   2022-06-15 [1] CRAN (R 4.1.0)
##  paletteer            * 1.5.0     2022-10-19 [1] CRAN (R 4.1.3)
##  parallelly             1.32.1    2022-07-21 [1] CRAN (R 4.1.3)
##  patchwork            * 1.1.1     2020-12-17 [1] CRAN (R 4.1.0)
##  pbapply                1.5-0     2021-09-16 [1] CRAN (R 4.1.0)
##  phateR                 1.0.7     2021-02-12 [1] CRAN (R 4.1.0)
##  pillar                 1.8.0     2022-07-18 [1] CRAN (R 4.1.3)
##  pkgconfig              2.0.3     2019-09-22 [2] CRAN (R 4.1.3)
##  plotly                 4.10.0    2021-10-09 [1] CRAN (R 4.1.0)
##  plyr                   1.8.7     2022-03-24 [1] CRAN (R 4.1.0)
##  png                    0.1-7     2013-12-03 [2] CRAN (R 4.1.3)
##  polyclip               1.10-0    2019-03-14 [2] CRAN (R 4.1.3)
##  prettyunits            1.1.1     2020-01-24 [2] CRAN (R 4.1.3)
##  prismatic              1.1.0     2021-10-17 [1] CRAN (R 4.1.0)
##  processx               3.7.0     2022-07-07 [1] CRAN (R 4.1.3)
##  progress               1.2.2     2019-05-16 [2] CRAN (R 4.1.3)
##  progressr              0.10.1    2022-06-03 [1] CRAN (R 4.1.0)
##  promises               1.2.0.1   2021-02-11 [2] CRAN (R 4.1.3)
##  ps                     1.7.1     2022-06-18 [1] CRAN (R 4.1.0)
##  purrr                * 0.3.4     2020-04-17 [2] CRAN (R 4.1.3)
##  R6                     2.5.1     2021-08-19 [1] CRAN (R 4.1.0)
##  ragg                   1.2.2     2022-02-21 [1] CRAN (R 4.1.0)
##  RANN                   2.6.1     2019-01-08 [2] CRAN (R 4.1.3)
##  rappdirs               0.3.3     2021-01-31 [2] CRAN (R 4.1.3)
##  RColorBrewer           1.1-3     2022-04-03 [1] CRAN (R 4.1.3)
##  Rcpp                   1.0.9     2022-07-08 [1] CRAN (R 4.1.3)
##  RcppAnnoy              0.0.19    2021-07-30 [1] CRAN (R 4.1.0)
##  RcppZiggurat           0.1.6     2020-10-20 [1] CRAN (R 4.1.3)
##  RCurl                  1.98-1.7  2022-06-09 [1] CRAN (R 4.1.0)
##  readr                * 2.1.2     2022-01-30 [1] CRAN (R 4.1.0)
##  readxl                 1.4.0     2022-03-28 [1] CRAN (R 4.1.0)
##  rematch2               2.1.2     2020-05-01 [2] CRAN (R 4.1.3)
##  reprex                 2.0.2     2022-08-17 [2] CRAN (R 4.1.3)
##  reshape2               1.4.4     2020-04-09 [2] CRAN (R 4.1.3)
##  reticulate           * 1.25      2022-05-11 [1] CRAN (R 4.1.3)
##  Rfast                  2.0.6     2022-02-16 [1] CRAN (R 4.1.3)
##  rgdal                  1.5-32    2022-05-09 [1] CRAN (R 4.1.3)
##  rgeos                  0.5-9     2021-12-15 [2] CRAN (R 4.1.3)
##  rlang                  1.0.4     2022-07-12 [1] CRAN (R 4.1.3)
##  rmarkdown              2.14      2022-04-25 [1] CRAN (R 4.1.3)
##  ROCR                   1.0-11    2020-05-02 [2] CRAN (R 4.1.3)
##  rpart                  4.1.16    2022-01-24 [2] CRAN (R 4.1.3)
##  rprojroot              2.0.3     2022-04-02 [1] CRAN (R 4.1.3)
##  RSQLite                2.2.15    2022-07-17 [1] CRAN (R 4.1.3)
##  rstudioapi             0.14      2022-08-22 [2] CRAN (R 4.1.3)
##  Rtsne                  0.16      2022-04-17 [1] CRAN (R 4.1.3)
##  rvest                  1.0.2     2021-10-16 [1] CRAN (R 4.1.0)
##  S4Vectors            * 0.34.0    2022-04-26 [1] Bioconductor
##  sass                   0.4.2     2022-07-16 [1] CRAN (R 4.1.3)
##  scaffold             * 0.2.0     2022-10-28 [1] Github (rhondabacher/scaffold@714c319)
##  scales                 1.2.0     2022-04-13 [1] CRAN (R 4.1.3)
##  scattermore            0.8       2022-02-14 [1] CRAN (R 4.1.0)
##  SCISSORS             * 1.2.0     2022-11-12 [1] Github (jr-leary7/SCISSORS@3459cf5)
##  sctransform            0.3.4     2022-08-20 [2] CRAN (R 4.1.3)
##  segmented            * 1.6-0     2022-05-31 [1] CRAN (R 4.1.0)
##  sessioninfo            1.2.2     2021-12-06 [2] CRAN (R 4.1.3)
##  Seurat               * 4.1.1     2022-05-02 [1] CRAN (R 4.1.3)
##  SeuratObject         * 4.1.0     2022-05-01 [1] CRAN (R 4.1.3)
##  shiny                  1.7.2     2022-07-19 [1] CRAN (R 4.1.3)
##  SingleCellExperiment * 1.16.0    2021-10-26 [2] Bioconductor
##  sp                   * 1.5-0     2022-06-05 [1] CRAN (R 4.1.0)
##  spatstat.core          2.4-4     2022-05-18 [1] CRAN (R 4.1.0)
##  spatstat.data          3.0-0     2022-10-21 [1] CRAN (R 4.1.3)
##  spatstat.geom          3.0-3     2022-10-25 [1] CRAN (R 4.1.3)
##  spatstat.random        3.0-1     2022-11-03 [1] CRAN (R 4.1.3)
##  spatstat.sparse        3.0-0     2022-10-21 [1] CRAN (R 4.1.3)
##  spatstat.utils         3.0-1     2022-10-19 [1] CRAN (R 4.1.3)
##  stringi                1.7.8     2022-07-11 [1] CRAN (R 4.1.3)
##  stringr              * 1.4.1     2022-08-20 [2] CRAN (R 4.1.3)
##  SummarizedExperiment * 1.24.0    2021-10-26 [2] Bioconductor
##  survival               3.3-1     2022-03-03 [1] CRAN (R 4.1.0)
##  systemfonts            1.0.4     2022-02-11 [1] CRAN (R 4.1.0)
##  tarchetypes          * 0.7.1     2022-09-07 [1] CRAN (R 4.1.3)
##  targets              * 0.13.5    2022-09-26 [1] CRAN (R 4.1.3)
##  tensor                 1.5       2012-05-05 [2] CRAN (R 4.1.3)
##  textshaping            0.3.6     2021-10-13 [2] CRAN (R 4.1.3)
##  tibble               * 3.1.8     2022-07-22 [1] CRAN (R 4.1.3)
##  tidyr                * 1.2.0     2022-02-01 [1] CRAN (R 4.1.3)
##  tidyselect             1.1.2     2022-02-21 [1] CRAN (R 4.1.0)
##  tidyverse            * 1.3.2     2022-07-18 [1] CRAN (R 4.1.3)
##  tzdb                   0.3.0     2022-03-28 [1] CRAN (R 4.1.0)
##  utf8                   1.2.2     2021-07-24 [1] CRAN (R 4.1.0)
##  uwot                   0.1.11    2021-12-02 [1] CRAN (R 4.1.0)
##  vctrs                  0.4.1     2022-04-13 [1] CRAN (R 4.1.3)
##  viridisLite            0.4.1     2022-08-22 [2] CRAN (R 4.1.3)
##  withr                  2.5.0     2022-03-03 [1] CRAN (R 4.1.3)
##  wrswoR                 1.1.1     2020-07-26 [1] CRAN (R 4.1.3)
##  xfun                   0.31      2022-05-10 [1] CRAN (R 4.1.3)
##  XML                    3.99-0.10 2022-06-09 [1] CRAN (R 4.1.0)
##  xml2                   1.3.3     2021-11-30 [2] CRAN (R 4.1.3)
##  xtable                 1.8-4     2019-04-21 [2] CRAN (R 4.1.3)
##  XVector                0.34.0    2021-10-26 [2] Bioconductor
##  yaml                   2.3.5     2022-02-21 [1] CRAN (R 4.1.0)
##  zlibbioc               1.40.0    2021-10-26 [2] Bioconductor
##  zoo                    1.8-10    2022-04-15 [1] CRAN (R 4.1.3)
## 
##  [1] /nas/longleaf/home/jrleary/r_packages_default
##  [2] /nas/longleaf/rhel8/apps/r/4.1.3/lib64/R/library
## 
## ─ Python configuration ───────────────────────────────────────────────────────
##  python:         /nas/longleaf/home/jrleary/Python/bin/python
##  libpython:      /usr/lib64/libpython3.6m.so
##  pythonhome:     /nas/longleaf/home/jrleary/Python:/nas/longleaf/home/jrleary/Python
##  version:        3.6.8 (default, Sep  9 2021, 07:49:02)  [GCC 8.5.0 20210514 (Red Hat 8.5.0-3)]
##  numpy:          /nas/longleaf/home/jrleary/Python/lib/python3.6/site-packages/numpy
##  numpy_version:  1.19.5
##  
##  NOTE: Python version was forced by use_python function
## 
## ──────────────────────────────────────────────────────────────────────────────
LS0tCnRpdGxlOiAiU0NJU1NPUlMgU2ltdWxhdGlvbiBTdHVkeSIKc3VidGl0bGU6ICJKYWNrIExlYXJ5LCBNLlMuIiAKYXV0aG9yOiAiVU5DIExpbmViZXJnZXIgQ29tcHJlaGVuc2l2ZSBDYW5jZXIgQ2VudGVyIC0gVUYgRGVwdC4gb2YgQmlvc3RhdGlzdGljcyIKZGF0ZTogImByIFN5cy5EYXRlKClgIgpvdXRwdXQ6CiAgaHRtbF9kb2N1bWVudDoKICAgIHRoZW1lOiBqb3VybmFsCiAgICBoaWdobGlnaHQ6IHRhbmdvCiAgICBjb2RlX2ZvbGRpbmc6IHNob3cKICAgIGNvZGVfZG93bmxvYWQ6IHRydWUgCiAgICB0b2M6IHRydWUKICAgIHRvY19kZXB0OiAyCiAgICB0b2NfZmxvYXQ6IGZhbHNlCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UpOyBzZXQuc2VlZCgzMTIpICAjIGx1Y2t5IHNlZWQKYGBgCgojIExpYnJhcmllcyAKCkZpcnN0IHdlJ2xsIGxvYWQgaW4gdGhlIHBhY2thZ2VzIHdlIG5lZWQgdG8gdGlkeSAmIGFuYWx5emUgb3VyIHNpbXVsYXRpb24gcmVzdWx0cy4gCgpgYGB7ciwgcmVzdWx0cz0naGlkZSd9CmxpYnJhcnkoZHBseXIpICAgICAgICMgZGF0YSBtYW5pcHVsYXRpb24gCmxpYnJhcnkoU2V1cmF0KSAgICAgICMgc2NSTkEtc2VxIHRvb2xzICYgZGF0YSBzdHJ1Y3R1cmVzCmxpYnJhcnkoZ2dwbG90MikgICAgICMgcGxvdHMKbGlicmFyeSh0YXJnZXRzKSAgICAgIyBwaXBlbGluZSB0b29scwpsaWJyYXJ5KHBhbGV0dGVlcikgICAjIHBsb3QgY29sb3JzIApsaWJyYXJ5KHBhdGNod29yaykgICAjIHBsb3QgbGF5b3V0cwpsaWJyYXJ5KHJldGljdWxhdGUpICAjIHB5dGhvbgpgYGAKCiMgRGF0YSAKCk5leHQgd2UnbGwgbG9hZCBpbiB0aGUgY2x1c3RlcmluZyBhbGdvcml0aG0gb3V0cHV0cyBmcm9tIG91dCBge3RhcmdldHN9YCBwaXBlbGluZS4gCgpgYGB7cn0KIyBwYW5jcmVhcyByZWZlcmVuY2UgCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGwxMDAwX25jbHVzdDMpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGwxMDAwX25jbHVzdDUpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGwzMDAwX25jbHVzdDMpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGwzMDAwX25jbHVzdDUpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGwzMDAwX25jbHVzdDcpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGw1MDAwX25jbHVzdDMpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGw1MDAwX25jbHVzdDUpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGw1MDAwX25jbHVzdDcpCnRhcl9sb2FkKGNsdXN0cmVzX3BhbmNfbmNlbGwxMDAwMF9uY2x1c3QzKQp0YXJfbG9hZChjbHVzdHJlc19wYW5jX25jZWxsMTAwMDBfbmNsdXN0NSkKdGFyX2xvYWQoY2x1c3RyZXNfcGFuY19uY2VsbDEwMDAwX25jbHVzdDcpCiMgbHVuZyByZWZlcmVuY2UKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDEwMDBfbmNsdXN0MykKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDEwMDBfbmNsdXN0NSkKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDEwMDBfbmNsdXN0NykKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDMwMDBfbmNsdXN0MykKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDMwMDBfbmNsdXN0NSkKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDMwMDBfbmNsdXN0NykKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDUwMDBfbmNsdXN0MykKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDUwMDBfbmNsdXN0NSkKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDUwMDBfbmNsdXN0NykKdGFyX2xvYWQoY2x1c3RyZXNfbHVuZ19uY2VsbDEwMDAwX25jbHVzdDMpCnRhcl9sb2FkKGNsdXN0cmVzX2x1bmdfbmNlbGwxMDAwMF9uY2x1c3Q1KQp0YXJfbG9hZChjbHVzdHJlc19sdW5nX25jZWxsMTAwMDBfbmNsdXN0NykKYGBgCgpOZXh0IHdlJ2xsIGNvZXJjZSBldmVyeXRoaW5nIGludG8gb25lIGJpZyBkYXRhZnJhbWUuIFdlJ3JlIG5vdCBnb2luZyB0byBpbmNsdWRlIHRoZSBMZWlkZW4gY2x1c3RlcmluZyByZXN1bHRzIHRoaXMgdGltZSBhcyB0aGV5J3JlIG5lYXJseSBpZGVudGljYWwgdG8gdGhlIExvdXZhaW4gcmVzdWx0cy4gCgpgYGB7cn0Kc2ltX3Jlc3VsdHMgPC0gcHVycnI6Om1hcChscygpW2dyZXBsKCJjbHVzdHJlcyIsIGxzKCkpXSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgZnVuY3Rpb24oeCkgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgZGYgPC0gZXZhbChhcy5zeW1ib2woeCkpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBkZiA8LSBtdXRhdGUoZGYsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlZmVyZW5jZSA9IGNhc2Vfd2hlbihzdHJpbmdyOjpzdHJfZGV0ZWN0KHgsICJwYW5jIikgfiAiUGFuY3JlYXMiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgVFJVRSB+ICJMdW5nIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGFzZXQgPSB4KQogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuKGRmKQogICAgICAgICAgICAgICAgICAgICAgICAgIH0pICU+JSAKICAgICAgICAgICAgICAgcHVycnI6OnJlZHVjZShyYmluZCkgJT4lIAogICAgICAgICAgICAgICBmaWx0ZXIobWV0aG9kICE9ICJMZWlkZW4gKFNldXJhdCkiKSAlPiUgCiAgICAgICAgICAgICAgIG11dGF0ZShtZXRob2QgPSBmYWN0b3IobWV0aG9kLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZXZlbHMgPSBjKCJLLW1lYW5zIChIYXJ0aWdhbi1Xb25nKSIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIkhpZXJhcmNoaWNhbCAoV2FyZCkiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJMb3V2YWluIChTZXVyYXQpIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiREJTQ0FOIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiR2luaUNsdXN0MyIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlNDSVNTT1JTIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGMoIkstbWVhbnMiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJIaWVyYXJjaGljYWwiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJMb3V2YWluIChTZXVyYXQpIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiREJTQ0FOIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiR2luaUNsdXN0MyIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlNDSVNTT1JTIikpLCAKICAgICAgICAgICAgICAgICAgICAgIHJ1bnRpbWVfbWludXRlcyA9IGNhc2Vfd2hlbihydW50aW1lX3VuaXRzID09ICJzZWNzIiB+IHJ1bnRpbWUgLyA2MCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcnVudGltZV91bml0cyA9PSAibWlucyIgfiBydW50aW1lLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBydW50aW1lX3VuaXRzID09ICJob3VycyIgfiBydW50aW1lICogNjAsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFRSVUUgfiBOQV9yZWFsXykpCmBgYAoKIyBBbmFseXNpcyAKCiMjIENsYXNzaWZpY2F0aW9uIEVycm9yCgpGaXJzdCB3ZSdsbCBsb29rIGF0IHRoZSBkaXN0cmlidXRpb24gb2YgQVJJIHZhbHVlcyBhY2hpZXZlZCBieSBlYWNoIG1ldGhvZCBvdmVyIHRoZSBlbnRpcmUgcGFyYW1ldGVyIHNwYWNlLiBXZSBub3RlIHRoYXQgdGhpcyBpbmNsdWRlcyBwYXJhbWV0ZXIgdmFsdWVzIHRoYXQgYXJlIGxlc3MgdGhhbiBvcHRpbWFsIGZvciBzb21lIG1ldGhvZHMsIGJ1dCB0aGlzIGlzIHNvbWV3aGF0IHJlYWxpc3RpYyBhcyBpdCBjYW4gYmUgdmVyeSBkaWZmaWN1bHQgdG8gb2J0YWluIHBhcmFtZXRlciBvcHRpbWFsaXR5IHdpdGhvdXQgZ3JvdW5kIHRydXRoIGxhYmVscyBmb3IgeW91ciBkYXRhLiBJbiBwcmFjdGljZSBzdWItb3B0aW1hbCBwYXJhbWV0ZXJzIGFyZSBvZnRlbiBjaG9zZW4gZHVlIHRvIHJlbGlhbmNlIG9uIHNvZnR3YXJlIGRlZmF1bHRzLCByZXNlYXJjaGVyIGluZXhwZXJpZW5jZSwgb3Igc3RvY2hhc3RpY2l0eSBpbiBtZXRob2RzIC8gYW5hbHlzaXMuIAoKYGBge3IsIGZpZy53aWR0aD0xMH0KcDBhIDwtIGdncGxvdChzaW1fcmVzdWx0cywgYWVzKHggPSBtZXRob2QsIHkgPSBhcmksIGZpbGwgPSBtZXRob2QpKSArIAogICAgICAgZmFjZXRfd3JhcCh+cmVmZXJlbmNlKSArIAogICAgICAgZ2VvbV92aW9saW4oZHJhd19xdWFudGlsZXMgPSAwLjUsIAogICAgICAgICAgICAgICAgICAgY29sb3IgPSAiYmxhY2siLCAKICAgICAgICAgICAgICAgICAgIHNpemUgPSAxKSArIAogICAgICAgZ2dzaWduaWY6Omdlb21fc2lnbmlmKGNvbXBhcmlzb25zID0gbGlzdChjKCJTQ0lTU09SUyIsICJMb3V2YWluIChTZXVyYXQpIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJEQlNDQU4iKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkdpbmlDbHVzdDMiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkstbWVhbnMiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkhpZXJhcmNoaWNhbCIpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdCA9ICJ3aWxjb3gudGVzdCIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZXBfaW5jcmVhc2UgPSAwLjA4LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1hcF9zaWduaWZfbGV2ZWwgPSBUUlVFLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2anVzdCA9IDAuMiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0c2l6ZSA9IDMpICsgCiAgICAgICBsYWJzKGZpbGwgPSAiQ2x1c3RlcmluZyBNZXRob2QiLCB5ID0gIkFkanVzdGVkIFJhbmQgSW5kZXgiKSArIAogICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoKSwgYnJlYWtzID0gc2VxKDAsIDEsIGJ5ID0gMC4yNSkpICsgCiAgICAgICBzY2FsZV9maWxsX3BhbGV0dGVlcl9kKCJnZ3NjaTo6bnJjX25wZyIpICsgCiAgICAgICB0aGVtZV9jbGFzc2ljKGJhc2Vfc2l6ZSA9IDE0KSAgKyAKICAgICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNDUsIGhqdXN0ID0gMC45LCB2anVzdCA9IDAuOSksIAogICAgICAgICAgICAgYXhpcy50aXRsZS54ID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgIHBhbmVsLmdyaWQubWFqb3IueSA9IGVsZW1lbnRfbGluZSgpKQpwMGEKYGBgCgpXZSBjYW4gbWFrZSB0aGUgc2FtZSBwbG90IGZvciBvdmVyYWxsIHBlcmZvcm1hbmNlLCB3aXRob3V0IHNwbGl0dGluZyBieSByZWZlcmVuY2UgZGF0YXNldC4gCgpgYGB7ciwgZmlnLndpZHRoPTEwfQpwMGIgPC0gZ2dwbG90KHNpbV9yZXN1bHRzLCBhZXMoeCA9IG1ldGhvZCwgeSA9IGFyaSwgZmlsbCA9IG1ldGhvZCkpICsgCiAgICAgICBnZW9tX3Zpb2xpbihkcmF3X3F1YW50aWxlcyA9IDAuNSwgCiAgICAgICAgICAgICAgICAgICBjb2xvciA9ICJibGFjayIsIAogICAgICAgICAgICAgICAgICAgc2l6ZSA9IDEpICsgCiAgICAgICBnZ3NpZ25pZjo6Z2VvbV9zaWduaWYoY29tcGFyaXNvbnMgPSBsaXN0KGMoIlNDSVNTT1JTIiwgIkxvdXZhaW4gKFNldXJhdCkiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkRCU0NBTiIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygiU0NJU1NPUlMiLCAiR2luaUNsdXN0MyIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygiU0NJU1NPUlMiLCAiSy1tZWFucyIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygiU0NJU1NPUlMiLCAiSGllcmFyY2hpY2FsIikpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXN0ID0gIndpbGNveC50ZXN0IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RlcF9pbmNyZWFzZSA9IDAuMDgsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWFwX3NpZ25pZl9sZXZlbCA9IFRSVUUsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZqdXN0ID0gMC4yLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRleHRzaXplID0gMykgKyAKICAgICAgIGxhYnMoZmlsbCA9ICJDbHVzdGVyaW5nIE1ldGhvZCIsIHkgPSAiQWRqdXN0ZWQgUmFuZCBJbmRleCIpICsgCiAgICAgICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdCgpLCBicmVha3MgPSBzZXEoMCwgMSwgYnkgPSAwLjI1KSkgKyAKICAgICAgIHNjYWxlX2ZpbGxfcGFsZXR0ZWVyX2QoImdnc2NpOjpucmNfbnBnIikgKyAKICAgICAgIHRoZW1lX2NsYXNzaWMoYmFzZV9zaXplID0gMTQpICArIAogICAgICAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA0NSwgaGp1c3QgPSAwLjksIHZqdXN0ID0gMC45KSwgCiAgICAgICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCksIAogICAgICAgICAgICAgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKCkpCnAwYgpgYGAKCgpXZSdsbCByZXBlYXQgdGhlIHBsb3QgZm9yIHRoZSBOb3JtYWxpemVkIE11dHVhbCBJbmZvcm1hdGlvbi4gCgpgYGB7ciwgZmlnLndpZHRoPTEwfQpwMWEgPC0gZ2dwbG90KHNpbV9yZXN1bHRzLCBhZXMoeCA9IG1ldGhvZCwgeSA9IG5taSwgZmlsbCA9IG1ldGhvZCkpICsgCiAgICAgICBmYWNldF93cmFwKH5yZWZlcmVuY2UpICsgCiAgICAgICBnZW9tX3Zpb2xpbihkcmF3X3F1YW50aWxlcyA9IDAuNSwgCiAgICAgICAgICAgICAgICAgICBjb2xvciA9ICJibGFjayIsIAogICAgICAgICAgICAgICAgICAgc2l6ZSA9IDEpICsgCiAgICAgICBnZ3NpZ25pZjo6Z2VvbV9zaWduaWYoY29tcGFyaXNvbnMgPSBsaXN0KGMoIlNDSVNTT1JTIiwgIkxvdXZhaW4gKFNldXJhdCkiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJEQlNDQU4iKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJHaW5pQ2x1c3QzIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygiU0NJU1NPUlMiLCAiSy1tZWFucyIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkhpZXJhcmNoaWNhbCIpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdCA9ICJ3aWxjb3gudGVzdCIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZXBfaW5jcmVhc2UgPSAwLjA4LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1hcF9zaWduaWZfbGV2ZWwgPSBUUlVFLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2anVzdCA9IDAuMiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0c2l6ZSA9IDMpICsgCiAgICAgICBsYWJzKGZpbGwgPSAiQ2x1c3RlcmluZyBNZXRob2QiLCB5ID0gIk5vcm1hbGllZCBNdXR1YWwgSW5mb3JtYXRpb24iKSArIAogICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoKSwgYnJlYWtzID0gc2VxKDAsIDEsIGJ5ID0gMC4yNSkpICsgCiAgICAgICBzY2FsZV9maWxsX3BhbGV0dGVlcl9kKCJnZ3NjaTo6bnJjX25wZyIpICsgCiAgICAgICB0aGVtZV9jbGFzc2ljKGJhc2Vfc2l6ZSA9IDE0KSAgKyAKICAgICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNDUsIGhqdXN0ID0gMC45LCB2anVzdCA9IDAuOSksIAogICAgICAgICAgICAgYXhpcy50aXRsZS54ID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgIHBhbmVsLmdyaWQubWFqb3IueSA9IGVsZW1lbnRfbGluZSgpKQpwMWEKYGBgCgpBZ2FpbiwgdGhlIGVudGlyZSBkYXRhc2V0IHdpdGhvdXQgc3BsaXR0aW5nIGJ5IHJlZmVyZW5jZS4gCgpgYGB7ciwgZmlnLndpZHRoPTEwfQpwMWIgPC0gZ2dwbG90KHNpbV9yZXN1bHRzLCBhZXMoeCA9IG1ldGhvZCwgeSA9IG5taSwgZmlsbCA9IG1ldGhvZCkpICsgCiAgICAgICBnZW9tX3Zpb2xpbihkcmF3X3F1YW50aWxlcyA9IDAuNSwgCiAgICAgICAgICAgICAgICAgICBjb2xvciA9ICJibGFjayIsIAogICAgICAgICAgICAgICAgICAgc2l6ZSA9IDEpICsgCiAgICAgICBnZ3NpZ25pZjo6Z2VvbV9zaWduaWYoY29tcGFyaXNvbnMgPSBsaXN0KGMoIlNDSVNTT1JTIiwgIkxvdXZhaW4gKFNldXJhdCkiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJEQlNDQU4iKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJHaW5pQ2x1c3QzIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygiU0NJU1NPUlMiLCAiSy1tZWFucyIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkhpZXJhcmNoaWNhbCIpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdCA9ICJ3aWxjb3gudGVzdCIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZXBfaW5jcmVhc2UgPSAwLjA4LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1hcF9zaWduaWZfbGV2ZWwgPSBUUlVFLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2anVzdCA9IDAuMiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0c2l6ZSA9IDMpICsgCiAgICAgICBsYWJzKGZpbGwgPSAiQ2x1c3RlcmluZyBNZXRob2QiLCB5ID0gIk5vcm1hbGllZCBNdXR1YWwgSW5mb3JtYXRpb24iKSArIAogICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoKSwgYnJlYWtzID0gc2VxKDAsIDEsIGJ5ID0gMC4yNSkpICsgCiAgICAgICBzY2FsZV9maWxsX3BhbGV0dGVlcl9kKCJnZ3NjaTo6bnJjX25wZyIpICsgCiAgICAgICB0aGVtZV9jbGFzc2ljKGJhc2Vfc2l6ZSA9IDE0KSAgKyAKICAgICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNDUsIGhqdXN0ID0gMC45LCB2anVzdCA9IDAuOSksIAogICAgICAgICAgICAgYXhpcy50aXRsZS54ID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgIHBhbmVsLmdyaWQubWFqb3IueSA9IGVsZW1lbnRfbGluZSgpKQpwMWIKYGBgCgpBbmQgbGFzdGx5IGZvciB0aGUgbWVhbiBzaWxob3VldHRlIHNjb3JlLiAKCmBgYHtyLCBmaWcud2lkdGg9MTB9CnAyYSA8LSBnZ3Bsb3Qoc2ltX3Jlc3VsdHMsIGFlcyh4ID0gbWV0aG9kLCB5ID0gc2lsLCBmaWxsID0gbWV0aG9kKSkgKyAKICAgICAgIGZhY2V0X3dyYXAofnJlZmVyZW5jZSkgKyAKICAgICAgIGdlb21fdmlvbGluKGRyYXdfcXVhbnRpbGVzID0gMC41LCAKICAgICAgICAgICAgICAgICAgIGNvbG9yID0gImJsYWNrIiwgCiAgICAgICAgICAgICAgICAgICBzaXplID0gMSkgKyAKICAgICAgIGdnc2lnbmlmOjpnZW9tX3NpZ25pZihjb21wYXJpc29ucyA9IGxpc3QoYygiU0NJU1NPUlMiLCAiTG91dmFpbiAoU2V1cmF0KSIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygiU0NJU1NPUlMiLCAiREJTQ0FOIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJHaW5pQ2x1c3QzIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJLLW1lYW5zIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJIaWVyYXJjaGljYWwiKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlc3QgPSAid2lsY294LnRlc3QiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGVwX2luY3JlYXNlID0gMC4wOCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXBfc2lnbmlmX2xldmVsID0gVFJVRSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmp1c3QgPSAwLjIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dHNpemUgPSAzKSArIAogICAgICAgbGFicyhmaWxsID0gIkNsdXN0ZXJpbmcgTWV0aG9kIiwgeSA9ICJNZWFuIFNpbGhvdWV0dGUgU2NvcmUiKSArIAogICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6bnVtYmVyX2Zvcm1hdChhY2N1cmFjeSA9IC4xKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWtzID0gc2VxKHJvdW5kKG1pbihzaW1fcmVzdWx0cyRzaWwpKSwgMSwgYnkgPSAwLjI1KSkgKyAKICAgICAgIHNjYWxlX2ZpbGxfcGFsZXR0ZWVyX2QoImdnc2NpOjpucmNfbnBnIikgKyAKICAgICAgIHRoZW1lX2NsYXNzaWMoYmFzZV9zaXplID0gMTQpICArIAogICAgICAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA0NSwgaGp1c3QgPSAwLjksIHZqdXN0ID0gMC45KSwgCiAgICAgICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCksIAogICAgICAgICAgICAgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKCkpCnAyYQpgYGAKCldpdGhvdXQgc3BsaXR0aW5nIGJ5IHJlZmVyZW5jZToKCmBgYHtyLCBmaWcud2lkdGg9MTB9CnAyYiA8LSBnZ3Bsb3Qoc2ltX3Jlc3VsdHMsIGFlcyh4ID0gbWV0aG9kLCB5ID0gc2lsLCBmaWxsID0gbWV0aG9kKSkgKyAKICAgICAgIGdlb21fdmlvbGluKGRyYXdfcXVhbnRpbGVzID0gMC41LCAKICAgICAgICAgICAgICAgICAgIGNvbG9yID0gImJsYWNrIiwgCiAgICAgICAgICAgICAgICAgICBzaXplID0gMSkgKyAKICAgICAgIGdnc2lnbmlmOjpnZW9tX3NpZ25pZihjb21wYXJpc29ucyA9IGxpc3QoYygiU0NJU1NPUlMiLCAiTG91dmFpbiAoU2V1cmF0KSIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygiU0NJU1NPUlMiLCAiREJTQ0FOIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJHaW5pQ2x1c3QzIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJLLW1lYW5zIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJIaWVyYXJjaGljYWwiKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlc3QgPSAid2lsY294LnRlc3QiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGVwX2luY3JlYXNlID0gMC4wOCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXBfc2lnbmlmX2xldmVsID0gVFJVRSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmp1c3QgPSAwLjIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dHNpemUgPSAzKSArIAogICAgICAgbGFicyhmaWxsID0gIkNsdXN0ZXJpbmcgTWV0aG9kIiwgeSA9ICJNZWFuIFNpbGhvdWV0dGUgU2NvcmUiKSArIAogICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6bnVtYmVyX2Zvcm1hdChhY2N1cmFjeSA9IC4xKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgYnJlYWtzID0gc2VxKHJvdW5kKG1pbihzaW1fcmVzdWx0cyRzaWwpKSwgMSwgYnkgPSAwLjI1KSkgKyAKICAgICAgIHNjYWxlX2ZpbGxfcGFsZXR0ZWVyX2QoImdnc2NpOjpucmNfbnBnIikgKyAKICAgICAgIHRoZW1lX2NsYXNzaWMoYmFzZV9zaXplID0gMTQpICArIAogICAgICAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA0NSwgaGp1c3QgPSAwLjksIHZqdXN0ID0gMC45KSwgCiAgICAgICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCksIAogICAgICAgICAgICAgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKCkpCnAyYgpgYGAKCk5leHQgd2UnbGwgc3Vic2V0IHRoZSBkYXRhIHRvIG9ubHkgaW5jbHVkZSBBUkkgdmFsdWVzIGFib3ZlIHRoZSBtZWRpYW4gZm9yIGVhY2ggbWV0aG9kLCB3aGljaCB3ZSBhcmUgZG9pbmcgaW4gb3JkZXIgdG8gbWltaWMgcmVhc29uYWJsZSBwYXJhbWV0ZXIgc2VsZWN0aW9uIGJ5IGV4cGVyaWVuY2VkIHJlc2VhcmNoZXJzLiAKCmBgYHtyLCBmaWcud2lkdGg9MTB9CnAzIDwtIHNpbV9yZXN1bHRzICU+JSAKICAgICAgd2l0aF9ncm91cHMoYyhyZWZlcmVuY2UsIG1ldGhvZCksIAogICAgICAgICAgICAgICAgICBmaWx0ZXIsIAogICAgICAgICAgICAgICAgICBhcmkgPiBtZWRpYW4oYXJpKSkgJT4lIAogICAgICBnZ3Bsb3QoYWVzKHggPSBtZXRob2QsIHkgPSBhcmksIGZpbGwgPSBtZXRob2QpKSArIAogICAgICBmYWNldF93cmFwKH5yZWZlcmVuY2UpICsgCiAgICAgIGdlb21fdmlvbGluKGRyYXdfcXVhbnRpbGVzID0gMC41LCAKICAgICAgICAgICAgICAgICAgY29sb3IgPSAiYmxhY2siLCAKICAgICAgICAgICAgICAgICAgc2l6ZSA9IDEpICsgCiAgICAgIGdnc2lnbmlmOjpnZW9tX3NpZ25pZihjb21wYXJpc29ucyA9IGxpc3QoYygiU0NJU1NPUlMiLCAiTG91dmFpbiAoU2V1cmF0KSIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYygiU0NJU1NPUlMiLCAiREJTQ0FOIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJHaW5pQ2x1c3QzIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJLLW1lYW5zIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJIaWVyYXJjaGljYWwiKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdCA9ICJ3aWxjb3gudGVzdCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGVwX2luY3JlYXNlID0gMC4wOCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1hcF9zaWduaWZfbGV2ZWwgPSBUUlVFLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZqdXN0ID0gMC4yLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dHNpemUgPSAzKSArIAogICAgICBsYWJzKGZpbGwgPSAiQ2x1c3RlcmluZyBNZXRob2QiLAogICAgICAgICAgIHkgPSAiQWRqdXN0ZWQgUmFuZCBJbmRleCIsIAogICAgICAgICAgIGNhcHRpb24gPSAiVG9wIDUwJSBvZiBjbHVzdGVyaW5nIHJ1bnMiKSArIAogICAgICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdCgpLCBicmVha3MgPSBzZXEoMCwgMSwgYnkgPSAwLjI1KSkgKyAKICAgICAgc2NhbGVfZmlsbF9wYWxldHRlZXJfZCgiZ2dzY2k6Om5yY19ucGciKSArIAogICAgICB0aGVtZV9jbGFzc2ljKGJhc2Vfc2l6ZSA9IDE0KSAgKyAKICAgICAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA0NSwgaGp1c3QgPSAwLjksIHZqdXN0ID0gMC45KSwgCiAgICAgICAgICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfYmxhbmsoKSwgCiAgICAgICAgICAgIHBhbmVsLmdyaWQubWFqb3IueSA9IGVsZW1lbnRfbGluZSgpLCAKICAgICAgICAgICAgcGxvdC5jYXB0aW9uID0gZWxlbWVudF90ZXh0KGZhY2UgPSAiaXRhbGljIikpCnAzCmBgYAoKV2UnbGwgcmVwZWF0IHRoZSBmaWx0ZXJlZCBwbG90IGZvciB0aGUgTk1JIG9mIGVhY2ggbWV0aG9kLgoKYGBge3IsIGZpZy53aWR0aD0xMH0KcDQgPC0gc2ltX3Jlc3VsdHMgJT4lIAogICAgICB3aXRoX2dyb3VwcyhjKHJlZmVyZW5jZSwgbWV0aG9kKSwgCiAgICAgICAgICAgICAgICAgIGZpbHRlciwgCiAgICAgICAgICAgICAgICAgIG5taSA+IG1lZGlhbihubWkpKSAlPiUgCiAgICAgIGdncGxvdChhZXMoeCA9IG1ldGhvZCwgeSA9IG5taSwgZmlsbCA9IG1ldGhvZCkpICsgCiAgICAgIGZhY2V0X3dyYXAofnJlZmVyZW5jZSkgKyAKICAgICAgZ2VvbV92aW9saW4oZHJhd19xdWFudGlsZXMgPSAwLjUsIAogICAgICAgICAgICAgICAgICBjb2xvciA9ICJibGFjayIsIAogICAgICAgICAgICAgICAgICBzaXplID0gMSkgKyAKICAgICAgZ2dzaWduaWY6Omdlb21fc2lnbmlmKGNvbXBhcmlzb25zID0gbGlzdChjKCJTQ0lTU09SUyIsICJMb3V2YWluIChTZXVyYXQpIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJEQlNDQU4iKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkdpbmlDbHVzdDMiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkstbWVhbnMiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkhpZXJhcmNoaWNhbCIpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXN0ID0gIndpbGNveC50ZXN0IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZXBfaW5jcmVhc2UgPSAwLjA4LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgbWFwX3NpZ25pZl9sZXZlbCA9IFRSVUUsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdmp1c3QgPSAwLjIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0c2l6ZSA9IDMpICsgCiAgICAgIGxhYnMoZmlsbCA9ICJDbHVzdGVyaW5nIE1ldGhvZCIsIAogICAgICAgICAgIHkgPSAiTm9ybWFsaXplZCBNdXR1YWwgSW5mb3JtYXRpb24iLCAKICAgICAgICAgICBjYXB0aW9uID0gIlRvcCA1MCUgb2YgY2x1c3RlcmluZyBydW5zIikgKyAKICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoKSwgYnJlYWtzID0gc2VxKDAsIDEsIGJ5ID0gMC4yNSkpICsgCiAgICAgIHNjYWxlX2ZpbGxfcGFsZXR0ZWVyX2QoImdnc2NpOjpucmNfbnBnIikgKyAKICAgICAgdGhlbWVfY2xhc3NpYyhiYXNlX3NpemUgPSAxNCkgICsgCiAgICAgIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNDUsIGhqdXN0ID0gMC45LCB2anVzdCA9IDAuOSksIAogICAgICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCksIAogICAgICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnkgPSBlbGVtZW50X2xpbmUoKSwgCiAgICAgICAgICAgIHBsb3QuY2FwdGlvbiA9IGVsZW1lbnRfdGV4dChmYWNlID0gIml0YWxpYyIpKQpwNApgYGAKCkFzIHdlbGwgYXMgZm9yIHRoZSBzaWxob3VldHRlIHNjb3JlLiAKCmBgYHtyLCBmaWcud2lkdGg9MTB9CnA1IDwtIHNpbV9yZXN1bHRzICU+JSAKICAgICAgd2l0aF9ncm91cHMoYyhyZWZlcmVuY2UsIG1ldGhvZCksIAogICAgICAgICAgICAgICAgICBmaWx0ZXIsIAogICAgICAgICAgICAgICAgICBzaWwgPiBtZWRpYW4oc2lsKSkgJT4lIAogICAgICBnZ3Bsb3QoYWVzKHggPSBtZXRob2QsIHkgPSBzaWwsIGZpbGwgPSBtZXRob2QpKSArIAogICAgICBmYWNldF93cmFwKH5yZWZlcmVuY2UpICsgCiAgICAgIGdlb21fdmlvbGluKGRyYXdfcXVhbnRpbGVzID0gMC41LAogICAgICAgICAgICAgICAgICBjb2xvciA9ICJibGFjayIsIAogICAgICAgICAgICAgICAgICBzaXplID0gMSkgKyAKICAgICAgZ2dzaWduaWY6Omdlb21fc2lnbmlmKGNvbXBhcmlzb25zID0gbGlzdChjKCJTQ0lTU09SUyIsICJMb3V2YWluIChTZXVyYXQpIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjKCJTQ0lTU09SUyIsICJEQlNDQU4iKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkdpbmlDbHVzdDMiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkstbWVhbnMiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGMoIlNDSVNTT1JTIiwgIkhpZXJhcmNoaWNhbCIpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXN0ID0gIndpbGNveC50ZXN0IiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZXBfaW5jcmVhc2UgPSAwLjA4LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgbWFwX3NpZ25pZl9sZXZlbCA9IFRSVUUsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdmp1c3QgPSAwLjIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0c2l6ZSA9IDMpICsgCiAgICAgIGxhYnMoZmlsbCA9ICJDbHVzdGVyaW5nIE1ldGhvZCIsIAogICAgICAgICAgIHkgPSAiU2lsaG91ZXR0ZSBTY29yZSIsIAogICAgICAgICAgIGNhcHRpb24gPSAiVG9wIDUwJSBvZiBjbHVzdGVyaW5nIHJ1bnMiKSArIAogICAgICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpudW1iZXJfZm9ybWF0KGFjY3VyYWN5ID0gLjEpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICBicmVha3MgPSBzZXEocm91bmQobWluKHNpbV9yZXN1bHRzJHNpbCkpLCAxLCBieSA9IDAuMjUpKSArIAogICAgICBzY2FsZV9maWxsX3BhbGV0dGVlcl9kKCJnZ3NjaTo6bnJjX25wZyIpICsgCiAgICAgIHRoZW1lX2NsYXNzaWMoYmFzZV9zaXplID0gMTQpICArIAogICAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDQ1LCBoanVzdCA9IDAuOSwgdmp1c3QgPSAwLjkpLCAKICAgICAgICAgICAgYXhpcy50aXRsZS54ID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgcGFuZWwuZ3JpZC5tYWpvci55ID0gZWxlbWVudF9saW5lKCksIAogICAgICAgICAgICBwbG90LmNhcHRpb24gPSBlbGVtZW50X3RleHQoZmFjZSA9ICJpdGFsaWMiKSkKcDUKYGBgCgojIyBTaW11bGF0aW9uIENhc2UgU3R1ZHkKCldlIGNhbiBhbHNvIHBsb3QgYW4gZXhhbXBsZSBvZiBob3cgdGhlIHZhcmlvdXMgbWV0aG9kcyBjYXRlZ29yaXplIGFuIGV4YW1wbGUgZGF0YXNldC4gV2UnbGwgbmVlZCB0byBydW4gZWFjaCBtZXRob2QgaW5kaXZpZHVhbGx5LCB1c2luZyB0aGUgc2FtZSBjb2RlIHRoYXQgd2UgZGlkIGluIG91ciBge3RhcmdldHN9YCBwaXBlbGluZS4gRmlyc3Qgd2UnbGwgbG9hZCBpbiBhIHNpbXVsYXRlZCBkYXRhc2V0IGZyb20gdGhlIHBhbmNyZWFzIHJlZmVyZW5jZSB3aXRoIDUsMDAwIGNlbGxzIGFuZCA3IGNsdXN0ZXJzLiAKCmBgYHtyfQp0YXJfbG9hZChzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0NykKYGBgCgpIZXJlJ3Mgd2hhdCB0aGUgZ3JvdW5kIHRydXRoIGxhYmVscyBmcm9tIG91ciBzaW11bGF0aW9ucyBsb29rIGxpa2UuIFNvbWUgb2YgdGhlIGNsdXN0ZXJzIGFyZSBlYXNpbHkgc2VwYXJhYmxlLCBhbmQgc29tZSBvdGhlcnMgYXJlIGxlc3Mgc28uIAoKYGBge3IsIGZpZy53aWR0aD04fQpzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0N0BtZXRhLmRhdGEkY2VsbFBvcHVsYXRpb24yIDwtIGFzLmludGVnZXIoc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDdAbWV0YS5kYXRhJGNlbGxQb3B1bGF0aW9uKSAtIDFMCnA2IDwtIERpbVBsb3Qoc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsIHJlZHVjdGlvbiA9ICJ0c25lIiwgZ3JvdXAuYnkgPSAiY2VsbFBvcHVsYXRpb24yIikgKyAKICAgICAgc2NhbGVfY29sb3JfcGFsZXR0ZWVyX2QoImdnc2NpOjpjYXRlZ29yeTEwX2QzIikgKyAKICAgICAgbGFicyh4ID0gInQtU05FIDEiLCAKICAgICAgICAgICB5ID0gInQtU05FIDIiLCAgCiAgICAgICAgICAgY29sb3IgPSAiVHJ1ZVxuTGFiZWwiKSArIAogICAgICB0aGVtZV9jbGFzc2ljKGJhc2Vfc2l6ZSA9IDE0KSArIAogICAgICB0aGVtZShheGlzLnRpY2tzID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgYXhpcy50ZXh0ID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfYmxhbmsoKSwgCiAgICAgICAgICAgIGxlZ2VuZC50aXRsZSA9IGVsZW1lbnRfdGV4dChmYWNlID0gImJvbGQiKSkgKyAKICAgICAgZ3VpZGVzKGNvbG9yID0gZ3VpZGVfbGVnZW5kKG92ZXJyaWRlLmFlcyA9IGxpc3Qoc2l6ZSA9IDMpKSkKcDYKYGBgCgpIZXJlIHdlIHJ1biB0aGUgYHtTQ0lTU09SU31gIHJlY2x1c3RlcmluZyBvdmVyIGFsbCBjbHVzdGVycywgb3B0aW1pemluZyBmb3IgbWF4aW11bSBzaWxob3VldHRlIHNjb3JlLiBGaXJzdCwgd2UgZXN0aW1hdGUgYW4gaW5pdGlhbCBicm9hZCBjbHVzdGVyaW5nLCB3aGljaCB3ZSBjYW4gY29tcGFyZSB0byB0aGUgdHJ1ZSBsYWJlbHMgYmVsb3cuICAKCmBgYHtyfQpzZXUgPC0gU2V1cmF0OjpGaW5kQ2x1c3RlcnMoc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVzb2x1dGlvbiA9IDAuMSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGdvcml0aG0gPSAxLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJhbmRvbS5zZWVkID0gMzEyLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSBGQUxTRSkKYGBgCgpOb3cgd2UgY2FuIHJlY2x1c3Rlci4gV2UnbGwgdXNlIHRoZSBkZWZhdWx0IHZhbHVlIG9mIHRoZSBtZWFuIHNpbGhvdWV0dGUgY3V0b2ZmIHNjb3JlLCAwLjI1LiBGb3Igb3RoZXIgcGFyYW1ldGVycywgd2UgdXNlIHRoZSBzYW1lIHNldHMgb2YgcG9zc2libGUgdmFsdWVzIHRoYXQgd2UgdXNlZCBpbiBldmFsdWF0aW5nIHBlcmZvcm1hbmNlIG9uIGFsbCBvdXIgc2ltdWxhdGVkIGRhdGFzZXRzLiBGb3IgdGhlIG51bWJlciBvZiBwcmluY2lwYWwgY29tcG9uZW50cywgd2UnbGwgdXNlIDIwIGFzIHRoZXJlJ3Mgbm90IHRvbyBtYW55IGNlbGxzIGluIHRoaXMgZGF0YXNldC4gCgpgYGB7cn0KU0NJU1NPUlNfY2x1c3RzIDwtIFNDSVNTT1JTOjpSZWNsdXN0ZXJDZWxscyhzZXVyYXQub2JqZWN0ID0gc2V1LCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhdXRvID0gVFJVRSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdXNlLnBhcmFsbGVsID0gRkFMU0UsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlc29sdXRpb24udmFscyA9IHNlcSgwLjEsIDAuNywgYnkgPSBjKDAuMSkpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrLnZhbHMgPSBjKDEwLCAyNSwgNDAsIDYwKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWVyZ2UuY2x1c3RlcnMgPSBGQUxTRSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdXNlLnNjdCA9IEZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuLkhWRyA9IDIwMDAsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG4uUEMgPSAyMCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY3V0b2ZmLnNjb3JlID0gMC4yNSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZG9tLnNlZWQgPSAzMTIpCnNldV9uZXcgPC0gU0NJU1NPUlM6OkludGVncmF0ZVN1YmNsdXN0ZXJzKG9yaWdpbmFsLm9iamVjdCA9IHNldSwgcmVjbHVzdC5yZXN1bHRzID0gU0NJU1NPUlNfY2x1c3RzKQpTQ0lTU09SU19jbHVzdHMgPC0gYXMuaW50ZWdlcihzZXVfbmV3JHNldXJhdF9jbHVzdGVycykgLSAxTApgYGAKCk5leHQgd2UgcnVuIExvdXZhaW4gY2x1c3RlcmluZyB3aXRoIGEgcmVhc29uYWJsZSByZXNvbHV0aW9uIHZhbHVlIG9mIDAuNS4gV2UnbGwgbWFrZSBzdXJlIHRvIGFsc28gdXNlIDIwIFBDcyBpbiB0aGUgY3JlYXRpb24gb2YgdGhlIFNOTiBncmFwaCBoZXJlLiAKCmBgYHtyfQpMb3V2YWluX2NsdXN0cyA8LSBTZXVyYXQ6OkZpbmROZWlnaGJvcnMoc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVkdWN0aW9uID0gInBjYSIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGltcyA9IDE6MjAsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbm4ubWV0aG9kID0gImFubm95IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbm5veS5tZXRyaWMgPSAiY29zaW5lIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlID0gRkFMU0UpICU+JSAKICAgICAgICAgICAgICAgICAgU2V1cmF0OjpGaW5kQ2x1c3RlcnMocmVzb2x1dGlvbiA9IDAuNSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsZ29yaXRobSA9IDEsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByYW5kb20uc2VlZCA9IDMxMiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSBGQUxTRSkKTG91dmFpbl9jbHVzdHMgPC0gYXMuaW50ZWdlcihMb3V2YWluX2NsdXN0cyRzZXVyYXRfY2x1c3RlcnMpIC0gMUwKYGBgCgpXZSdsbCBydW4gTGVpZGVuIGFzIHdlbGwsIHdpdGggdGhlIHNhbWUgcmVzb2x1dGlvbi4gCgpgYGB7cn0KcmV0aWN1bGF0ZTo6dXNlX3ZpcnR1YWxlbnYoIi9uYXMvbG9uZ2xlYWYvaG9tZS9qcmxlYXJ5L1B5dGhvbiIsIHJlcXVpcmVkID0gVFJVRSkKTGVpZGVuX2NsdXN0cyA8LSBTZXVyYXQ6OkZpbmRDbHVzdGVycyhzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0NywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVzb2x1dGlvbiA9IDAuNSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxnb3JpdGhtID0gNCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZG9tLnNlZWQgPSAzMTIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSBGQUxTRSkkc2V1cmF0X2NsdXN0ZXJzIApMZWlkZW5fY2x1c3RzIDwtIGFzLmludGVnZXIoTGVpZGVuX2NsdXN0cykgLSAxTApgYGAKCkZvciAkayQtbWVhbnMsIHdlJ2xsIHVzZSB0aGUgdHJ1ZSB2YWx1ZSAkayA9IDckLCBldmVuIHRob3VnaCBpbiBwcmFjdGljZSB3ZSB3b24ndCBrbm93IHRoYXQgcGFyYW1ldGVyLiAKCmBgYHtyfQprbWVhbnNfY2x1c3RzIDwtIGttZWFucyhFbWJlZGRpbmdzKHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3LCAicGNhIilbLCAxOjIwXSwKICAgICAgICAgICAgICAgICAgICAgICAgY2VudGVycyA9IDcsCiAgICAgICAgICAgICAgICAgICAgICAgIG5zdGFydCA9IDUsCiAgICAgICAgICAgICAgICAgICAgICAgIGFsZ29yaXRobSA9ICJIYXJ0aWdhbi1Xb25nIikkY2x1c3RlciAKa21lYW5zX2NsdXN0cyA8LSBhcy5pbnRlZ2VyKGttZWFuc19jbHVzdHMpIC0gMUwKYGBgCgpXZSdsbCBkbyB0aGUgc2FtZSB3aXRoIGhpZXJhcmNoaWNhbCBjbHVzdGVyaW5nLiAKCmBgYHtyfQpoY2x1c3RfdHJlZSA8LSBoY2x1c3QoU0NJU1NPUlM6OkNvc2luZURpc3QoRW1iZWRkaW5ncyhzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0NywgInBjYSIpWywgMToyMF0pLCBtZXRob2QgPSAid2FyZC5EMiIpCmhjbHVzdF9jbHVzdHMgPC0gYXMuaW50ZWdlcihjdXRyZWUoaGNsdXN0X3RyZWUsIGsgPSA3KSkgLSAxTApgYGAKCkxhc3RseSwgZm9yIERCU0NBTiAnbGwgZmlyc3QgY2hvb3NlIGEgcmVhc29uYWJsZSB2YWx1ZSBmb3IgdGhlIGVwc2lsb24gcGFyYW1ldGVyIGJ5IGxvb2tpbmcgZm9yIHRoZSBpbmZsZWN0aW9uIHBvaW50IGluIHRoZSBLTk4gZGlzdGFuY2UgcGxvdC4gSW4gb3VyIHNpbXVsYXRpb25zIHdlIGRpZCB0aGlzIGF1dG9tYXRpY2FsbHkgYnkgcnVubmluZyBzZXZlcmFsIHNlZ21lbnRlZCByZWdyZXNzaW9ucyBmb3IgY2hhbmdlcG9pbnQgZGV0ZWN0aW9uIGFuZCBpdGVyYXRpbmcgb3ZlciB0aG9zZSB2YWx1ZXMsIGJ1dCBpbiB0aGlzIGNhc2Ugd2UnbGwgY2hvb3NlIG9uZSBtYW51YWxseS4gSXQgbG9va3MgbGlrZSAkXGVwc2lsb24gPSA4JCBpcyBhIHJlYXNvbmFibGUgdmFsdWUuIAoKYGBge3IsIHJlc3VsdHM9J2hvbGQnfQpkYnNjYW46OmtOTmRpc3RwbG90KEVtYmVkZGluZ3Moc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsICJwY2EiKVssIDE6MjBdLCBrID0gMTApCmFibGluZShoID0gOCwgY29sID0gImZpcmVicmljayIsIGx0eSA9ICJkYXNoZWQiKQpkYnNjYW5fY2x1c3RzIDwtIGRic2Nhbjo6ZGJzY2FuKHNjYWxlKHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3QHJlZHVjdGlvbnMkcGNhQGNlbGwuZW1iZWRkaW5nc1ssIDE6MjBdLCBzY2FsZSA9IEZBTFNFKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZXBzID0gOCwgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1pblB0cyA9IDUsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlclBvaW50cyA9IFRSVUUpJGNsdXN0ZXIKYGBgCgpMYXN0bHkgd2UnbGwgcnVuIEdpbmlDbHVzdDMgdXNpbmcgdGhlIGRlZmF1bHQgcGFyYW1ldGVycy4gV2UnbGwgbmVlZCB0byBzd2l0Y2ggdG8gUHl0aG9uIHRvIHVzZSB0aGlzIG1ldGhvZCwgYnV0IGZpcnN0IHdlJ2xsIG5lZWQgdG8gbWFrZSBzdXJlIHdlIGNhbiByZWFkIHRoZSBjb3VudHMgbWF0cml4IGZyb20gUiB0byBQeXRob24uIAoKYGBge3J9CmNvdW50X21hdCA8LSBhcy5tYXRyaXgoc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDdAYXNzYXlzJFJOQUBjb3VudHMpCmBgYAoKTm93IHdlIGNhbiBydW4gdGhlIGFsZ29yaXRobS4gV2UnbGwgdXNlIHRoZSBkZWZhdWx0IHZhbHVlcyBmb3IgcGFyYW1ldGVycyBvZiBpbnRlcmVzdCBhcyBmb3VuZCBpbiBbdGhlIGB7R2luaUNsdXN0M31gIGRvY3NdKGh0dHBzOi8vZ2luaWNsdXN0My5yZWFkdGhlZG9jcy5pby9lbi9sYXRlc3QvKS4KCmBgYHtweXRob259CiMgaW1wb3J0IGxpYnJhcmllcyAKaW1wb3J0IGFubmRhdGEgICAgICAgICAgICMgYW5ub3RhdGVkIHNpbmdsZSBjZWxsIGRhdGEKaW1wb3J0IG51bXB5IGFzIG5wICAgICAgICMgbWF0cml4IGFsZ2VicmEKaW1wb3J0IHBhbmRhcyBhcyBwZCAgICAgICMgRGF0YUZyYW1lcwppbXBvcnQgc2NhbnB5IGFzIHNjICAgICAgIyBTY2FuUHkKaW1wb3J0IGdpbmljbHVzdDMgYXMgZ2MgICMgR2luaUNsdXN0MwojIGNyZWF0ZSBBbm5EYXRhIG9iamVjdAphZGF0YSA9IGFubmRhdGEuQW5uRGF0YShYPXIuY291bnRfbWF0LlQpCnNjLnBwLm5vcm1hbGl6ZV9wZXJfY2VsbChhZGF0YSwgY291bnRzX3Blcl9jZWxsX2FmdGVyPTFlNCkKIyBjYWxjdWxhdGUgY2x1c3RlcmluZyAKZ2MuZ2luaS5jYWxHaW5pKGFkYXRhLCBzZWxlY3Rpb249J3BfdmFsdWUnLCBwX3ZhbHVlPTAuMDAxKQpnYy5mYW5vLmNhbEZhbm8oYWRhdGEsIG1ldGhvZD0nc2NhbnB5JykKYWRhdGFHaW5pID0gZ2MuZ2luaS5jbHVzdGVyR2luaShhZGF0YSwgcmVzb2x1dGlvbj0wLjEsIG5laWdoYm9ycz01KQphZGF0YUZhbm8gPSBnYy5mYW5vLmNsdXN0ZXJGYW5vKGFkYXRhLCByZXNvbHV0aW9uPTAuMSwgbmVpZ2hib3JzPTE1KQpjb25zZW5zdXNDbHVzdGVyID0ge30KY29uc2Vuc3VzQ2x1c3RlclsnZ2luaUNsdXN0ZXInXSA9IG5wLmFycmF5KGFkYXRhLm9ic1sncmFyZSddLnZhbHVlcy50b2xpc3QoKSkKY29uc2Vuc3VzQ2x1c3RlclsnZmFub0NsdXN0ZXInXSA9IG5wLmFycmF5KGFkYXRhLm9ic1snZmFubyddLnZhbHVlcy50b2xpc3QoKSkKZ2MuY29uc2Vuc3VzLmdlbmVyYXRlTXRpbGRlKGNvbnNlbnN1c0NsdXN0ZXIpCmdjLmNvbnNlbnN1cy5jbHVzdGVyTXRpbGRlKGNvbnNlbnN1c0NsdXN0ZXIpCmBgYAoKV2Ugbm93IGJyaW5nIHRoZSBjbHVzdGVyaW5nIHJlc3VsdHMgYmFjayBpbnRvIFIuIAoKYGBge3J9CmdpbmljbHVzdDNfY2x1c3RzIDwtIGFzLmludGVnZXIocHkkY29uc2Vuc3VzQ2x1c3RlciRmaW5hbENsdXN0ZXIpCmBgYAoKTm93IGxldCdzIGJyaW5nIGFsbCBvdXIgdmFsdWUgdG9nZXRoZXIgJiBwbG90IHRoZW0uIAoKYGBge3IsIGZpZy53aWR0aD0xM30KY2x1c3RfY29tcCA8LSBkYXRhLmZyYW1lKFNDSVNTT1JTID0gU0NJU1NPUlNfY2x1c3RzLCAKICAgICAgICAgICAgICAgICAgICAgICAgIExvdXZhaW4gPSBMb3V2YWluX2NsdXN0cywgCiAgICAgICAgICAgICAgICAgICAgICAgICBMZWlkZW4gPSBMZWlkZW5fY2x1c3RzLCAKICAgICAgICAgICAgICAgICAgICAgICAgIEttZWFucyA9IGttZWFuc19jbHVzdHMsIAogICAgICAgICAgICAgICAgICAgICAgICAgSGllcmFyY2hpY2FsID0gaGNsdXN0X2NsdXN0cywgCiAgICAgICAgICAgICAgICAgICAgICAgICBEQlNDQU4gPSBkYnNjYW5fY2x1c3RzLCAKICAgICAgICAgICAgICAgICAgICAgICAgIEdpbmlDbHVzdDMgPSBnaW5pY2x1c3QzX2NsdXN0cywgCiAgICAgICAgICAgICAgICAgICAgICAgICB0U05FXzEgPSBFbWJlZGRpbmdzKHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3LCAidHNuZSIpWywgMV0sIAogICAgICAgICAgICAgICAgICAgICAgICAgdFNORV8yID0gRW1iZWRkaW5ncyhzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0NywgInRzbmUiKVssIDJdKSAlPiUgCiAgICAgICAgICAgICAgdGlkeXI6OnBpdm90X2xvbmdlcighY29udGFpbnMoInRTTkUiKSwgbmFtZXNfdG8gPSAibWV0aG9kIiwgdmFsdWVzX3RvID0gImNsdXN0ZXIiKSAlPiUgCiAgICAgICAgICAgICAgbXV0YXRlKGFjcm9zcyghY29udGFpbnMoInRTTkUiKSwgYXMuZmFjdG9yKSkgJT4lIAogICAgICAgICAgICAgIGZpbHRlcihtZXRob2QgIT0gIkxlaWRlbiIpCnA3IDwtIGdncGxvdChjbHVzdF9jb21wLCBhZXMoeCA9IHRTTkVfMSwgeSA9IHRTTkVfMiwgY29sb3IgPSBjbHVzdGVyKSkgKyAKICAgICAgZmFjZXRfd3JhcCh+bWV0aG9kKSArIAogICAgICBnZW9tX3BvaW50KHNpemUgPSAwLjc1KSArIAogICAgICBzY2FsZV9jb2xvcl9wYWxldHRlZXJfZCgiZ2dzY2k6OmNhdGVnb3J5MTBfZDMiKSArIAogICAgICBsYWJzKHggPSAidC1TTkUgMSIsIAogICAgICAgICAgIHkgPSAidC1TTkUgMiIsICAKICAgICAgICAgICBjb2xvciA9ICJFc3RpbWF0ZWRcbkNsdXN0ZXIiKSArIAogICAgICB0aGVtZV9jbGFzc2ljKGJhc2Vfc2l6ZSA9IDE0KSArIAogICAgICB0aGVtZShheGlzLnRpY2tzID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgYXhpcy50ZXh0ID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfYmxhbmsoKSwgCiAgICAgICAgICAgIGxlZ2VuZC50aXRsZSA9IGVsZW1lbnRfdGV4dChmYWNlID0gImJvbGQiKSkgKyAKICAgICAgZ3VpZGVzKGNvbG9yID0gZ3VpZGVfbGVnZW5kKG92ZXJyaWRlLmFlcyA9IGxpc3Qoc2l6ZSA9IDMpKSkKcDcKYGBgCgpXZSdsbCBhbHNvIHB1dCB0b2dldGhlciBzdWItdGFibGVzIG9mIHRoZSBBUkkgdmFsdWVzICYgbWVhbiBzaWxob3VldHRlIHNjb3JlcyBmb3IgZWFjaCBtZXRob2QuIAoKYGBge3J9CmFyaV9yZXMgPC0gZGF0YS5mcmFtZShBUkkgPSBjKG1jbHVzdDo6YWRqdXN0ZWRSYW5kSW5kZXgoc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDckY2VsbFBvcHVsYXRpb24sIFNDSVNTT1JTX2NsdXN0cyksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtY2x1c3Q6OmFkanVzdGVkUmFuZEluZGV4KHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3JGNlbGxQb3B1bGF0aW9uLCBMb3V2YWluX2NsdXN0cyksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtY2x1c3Q6OmFkanVzdGVkUmFuZEluZGV4KHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3JGNlbGxQb3B1bGF0aW9uLCBrbWVhbnNfY2x1c3RzKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1jbHVzdDo6YWRqdXN0ZWRSYW5kSW5kZXgoc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDckY2VsbFBvcHVsYXRpb24sIGhjbHVzdF9jbHVzdHMpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWNsdXN0OjphZGp1c3RlZFJhbmRJbmRleChzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0NyRjZWxsUG9wdWxhdGlvbiwgZGJzY2FuX2NsdXN0cyksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtY2x1c3Q6OmFkanVzdGVkUmFuZEluZGV4KHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3JGNlbGxQb3B1bGF0aW9uLCBnaW5pY2x1c3QzX2NsdXN0cykpLCAKICAgICAgICAgICAgICAgICAgICAgIE1ldGhvZCA9IGMoIlNDSVNTT1JTIiwgIkxvdXZhaW4iLCAiSy1tZWFucyIsICJIaWVyYXJjaGljYWwiLCAiREJTQ0FOIiwgIkdpbmlDbHVzdDMiKSkgJT4lIAogICAgICAgICAgIGFycmFuZ2UoZGVzYyhBUkkpKSAlPiUgCiAgICAgICAgICAgbXV0YXRlKEFSSV9SYW5rID0gcm93X251bWJlcigpKSAlPiUgCiAgICAgICAgICAgc2VsZWN0KE1ldGhvZCwgQVJJLCBBUklfUmFuaykKc2lsX3JlcyA8LSBkYXRhLmZyYW1lKFNpbCA9IGMobWVhbihjbHVzdGVyOjpzaWxob3VldHRlKFNDSVNTT1JTOjpDb3NpbmVEaXN0KEVtYmVkZGluZ3Moc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsICJwY2EiKVssIDE6MjBdKSwgeCA9IFNDSVNTT1JTX2NsdXN0cylbLCAzXSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZWFuKGNsdXN0ZXI6OnNpbGhvdWV0dGUoU0NJU1NPUlM6OkNvc2luZURpc3QoRW1iZWRkaW5ncyhzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0NywgInBjYSIpWywgMToyMF0pLCB4ID0gTG91dmFpbl9jbHVzdHMpWywgM10pLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWVhbihjbHVzdGVyOjpzaWxob3VldHRlKFNDSVNTT1JTOjpDb3NpbmVEaXN0KEVtYmVkZGluZ3Moc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsICJwY2EiKVssIDE6MjBdKSwgeCA9IGttZWFuc19jbHVzdHMpWywgM10pLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWVhbihjbHVzdGVyOjpzaWxob3VldHRlKFNDSVNTT1JTOjpDb3NpbmVEaXN0KEVtYmVkZGluZ3Moc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsICJwY2EiKVssIDE6MjBdKSwgeCA9IGhjbHVzdF9jbHVzdHMpWywgM10pLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWVhbihjbHVzdGVyOjpzaWxob3VldHRlKFNDSVNTT1JTOjpDb3NpbmVEaXN0KEVtYmVkZGluZ3Moc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsICJwY2EiKVssIDE6MjBdKSwgeCA9IGRic2Nhbl9jbHVzdHMpWywgM10pLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWVhbihjbHVzdGVyOjpzaWxob3VldHRlKFNDSVNTT1JTOjpDb3NpbmVEaXN0KEVtYmVkZGluZ3Moc2ltX3BhbmNfbmNlbGwxMDAwX25jbHVzdDcsICJwY2EiKVssIDE6MjBdKSwgeCA9IGdpbmljbHVzdDNfY2x1c3RzKVssIDNdKSksIAogICAgICAgICAgICAgICAgICAgICAgTWV0aG9kID0gYygiU0NJU1NPUlMiLCAiTG91dmFpbiIsICJLLW1lYW5zIiwgIkhpZXJhcmNoaWNhbCIsICJEQlNDQU4iLCAiR2luaUNsdXN0MyIpKSAlPiUgCiAgICAgICAgICAgYXJyYW5nZShkZXNjKFNpbCkpICU+JSAKICAgICAgICAgICBtdXRhdGUoU2lsX1JhbmsgPSByb3dfbnVtYmVyKCkpICU+JSAKICAgICAgICAgICBzZWxlY3QoTWV0aG9kLCBTaWwsIFNpbF9SYW5rKQphcmlfdGFibGUgPC0gZ3JpZEV4dHJhOjp0YWJsZUdyb2IoYXJpX3JlcywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbHMgPSBjKCJNZXRob2QiLCAiQWRqLiBSYW5kIEluZGV4IiwgIlJhbmsiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aGVtZSA9IGdyaWRFeHRyYTo6dHRoZW1lX21pbmltYWwoY29saGVhZCA9IGxpc3QoYmdfcGFyYW1zID0gbGlzdChmaWxsID0gImdyZXk5MCIpKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcm93cyA9IE5VTEwpCnNpbF90YWJsZSA8LSBncmlkRXh0cmE6OnRhYmxlR3JvYihzaWxfcmVzLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY29scyA9IGMoIk1ldGhvZCIsICJNZWFuIFNpbGhvdWV0dGUgU2NvcmUiLCAiUmFuayIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoZW1lID0gZ3JpZEV4dHJhOjp0dGhlbWVfbWluaW1hbChjb2xoZWFkID0gbGlzdChiZ19wYXJhbXMgPSBsaXN0KGZpbGwgPSAiZ3JleTkwIikpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByb3dzID0gTlVMTCkKbWV0cmljX3JlcyA8LSBhcmlfcmVzICU+JSAKICAgICAgICAgICAgICBpbm5lcl9qb2luKHNpbF9yZXMsIGJ5ID0gIk1ldGhvZCIpICU+JSAKICAgICAgICAgICAgICBtdXRhdGUoQ29tYmluZWRfUmFuayA9IEFSSV9SYW5rICsgU2lsX1JhbmspICU+JSAKICAgICAgICAgICAgICBhcnJhbmdlKENvbWJpbmVkX1JhbmspICU+JSAKICAgICAgICAgICAgICBtdXRhdGUoUmFuayA9IHJvd19udW1iZXIoKSkgJT4lIAogICAgICAgICAgICAgIHNlbGVjdChNZXRob2QsIAogICAgICAgICAgICAgICAgICAgICBBUkksIAogICAgICAgICAgICAgICAgICAgICBBUklfUmFuaywgCiAgICAgICAgICAgICAgICAgICAgIFNpbCwgCiAgICAgICAgICAgICAgICAgICAgIFNpbF9SYW5rLCAKICAgICAgICAgICAgICAgICAgICAgUmFuaykKbWV0cmljX3RhYmxlIDwtIGdyaWRFeHRyYTo6dGFibGVHcm9iKG1ldHJpY19yZXMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xzID0gYygiTWV0aG9kIiwgIkFSSSIsICJBUkkgUmFuayIsICJTaWxob3VldHRlIiwgIlNpbGhvdWV0dGUgUmFuayIsICJPdmVyYWxsIFJhbmsiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aGVtZSA9IGdyaWRFeHRyYTo6dHRoZW1lX21pbmltYWwoY29saGVhZCA9IGxpc3QoYmdfcGFyYW1zID0gbGlzdChmaWxsID0gImdyZXk5MCIpKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcm93cyA9IE5VTEwpCmBgYAoKTG9va2luZyBhdCB0aGUgcmVzdWx0cywgd2Ugc2VlIHRoYXQgYHtTQ0lTU09SU31gIGFsbW9zdCBleGFjdGx5IHJlY2FwdHVyZXMgdGhlIG9yaWdpbmFsIGNsdXN0ZXJpbmcsIGFuZCBhbHNvIHByb3ZpZGVzIHRoZSBiZXN0IG1lYXN1cmUgb2YgY2x1c3RlciBmaXQgKHNpbGhvdWV0dGUgc2NvcmUpLiBFdmVuIG1ldGhvZHMgdGhhdCBrbm93IHRoZSB0cnVlIG51bWJlciBvZiBjbHVzdGVyICphIHByaW9yaSosICRrJC1tZWFucyBhbmQgaGllcmFyY2hpY2FsIGNsdXN0ZXJpbmcsIGlnbm9yZSB0aGUgdHdvIHNtYWxsIHN1YmNsdXN0ZXJzIGluIGZhdm9yIG9mIHNwbGl0dGluZyB1cCBsYXJnZXIgY2x1c3RlcnMuIAoKYGBge3IsIGZpZy53aWR0aD0xNCwgZmlnLmhlaWdodD0xMH0KcDhhIDwtIChwNyAvICgoKHA2ICsgZmFjZXRfd3JhcCh+Ikdyb3VuZCBUcnV0aCIpKSB8IG1ldHJpY190YWJsZSkpKSArIAogICAgICAgcGxvdF9sYXlvdXQoaGVpZ2h0cyA9IGMoMiwgMSkpIApwOGEKYGBgCgpMYXN0bHksIHdlJ2xsIHNob3cgdGhhdCBzaW1wbHkgbmFpdmVseSBpbmNyZWFzaW5nIHRoZSByZXNvbHV0aW9uIHBhcmFtZXRlciBvZiB0aGUgTG91dmFpbiBjbHVzdGVyaW5nIGRvZXMgbm90IGxlYWQgdG8gYmV0dGVyIHJlc3VsdHMuIAoKYGBge3J9CkxvdXZhaW5fcmVzX2luY3IgPC0gcHVycnI6Om1hcChjKC41LCAuNywgLjksIDEuMSwgMS41LCAyKSwgZnVuY3Rpb24oeCkgewogIGNsdXN0ZXJfcmVzIDwtIFNldXJhdDo6RmluZENsdXN0ZXJzKHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3LCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXNvbHV0aW9uID0geCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxnb3JpdGhtID0gMSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmFuZG9tLnNlZWQgPSAzMTIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSBGQUxTRSkKICBjbHVzdGVyX3ZhbHMgPC0gYXMuaW50ZWdlcihjbHVzdGVyX3JlcyRzZXVyYXRfY2x1c3RlcnMpIC0gMUwKICBhcmlfdmFsIDwtIG1jbHVzdDo6YWRqdXN0ZWRSYW5kSW5kZXgoY2x1c3Rlcl9yZXMkc2V1cmF0X2NsdXN0ZXJzLCBzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0NyRjZWxsUG9wdWxhdGlvbikKICBzaWxfdmFsIDwtIG1lYW4oY2x1c3Rlcjo6c2lsaG91ZXR0ZShTQ0lTU09SUzo6Q29zaW5lRGlzdChFbWJlZGRpbmdzKHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3LCAicGNhIilbLCAxOjIwXSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHggPSBhcy5pbnRlZ2VyKGNsdXN0ZXJfcmVzJHNldXJhdF9jbHVzdGVycykgLSAxTClbLCAzXSkKICByZXR1cm4obGlzdChDbHVzdGVyaW5nID0gY2x1c3Rlcl92YWxzLCAKICAgICAgICAgICAgICBBUkkgPSBhcmlfdmFsLCAKICAgICAgICAgICAgICBTaWxob3VldHRlID0gc2lsX3ZhbCkpCn0pCnJlc19kZiA8LSBkYXRhLmZyYW1lKHJlc29sdXRpb24gPSByZXAoYyguNSwgLjcsIC45LCAxLjEsIDEuNSwgMiksIGVhY2ggPSBuY29sKHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3KSksIAogICAgICAgICAgICAgICAgICAgICBjbHVzdCA9IHVubGlzdChwdXJycjo6bWFwKExvdXZhaW5fcmVzX2luY3IsIFwoeCkgeCRDbHVzdGVyaW5nKSksIAogICAgICAgICAgICAgICAgICAgICB0U05FXzEgPSByZXAoRW1iZWRkaW5ncyhzaW1fcGFuY19uY2VsbDEwMDBfbmNsdXN0NywgInRzbmUiKVssIDFdLCA2KSwgCiAgICAgICAgICAgICAgICAgICAgIHRTTkVfMiA9IHJlcChFbWJlZGRpbmdzKHNpbV9wYW5jX25jZWxsMTAwMF9uY2x1c3Q3LCAidHNuZSIpWywgMl0sIDYpKSAlPiUgCiAgICAgICAgICBtdXRhdGUocmVzb2x1dGlvbiA9IGZhY3RvcihyZXNvbHV0aW9uLCBsYWJlbHMgPSBjKCIwLjUiLCAiMC43IiwgIjAuOSIsICIxLjEiLCAiMS41IiwgIjIuMCIpKSwgCiAgICAgICAgICAgICAgICAgY2x1c3QgPSBhcy5mYWN0b3IoY2x1c3QpKQpsb3V2YWluX21ldHJpY19yZXMgPC0gZGF0YS5mcmFtZShSZXMgPSBmYWN0b3IoYyguNSwgLjcsIC45LCAxLjEsIDEuNSwgMi4wKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsYWJlbHMgPSBjKCIwLjUiLCAiMC43IiwgIjAuOSIsICIxLjEiLCAiMS41IiwgIjIuMCIpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEFSSSA9IHB1cnJyOjptYXBfZGJsKExvdXZhaW5fcmVzX2luY3IsIFwoeCkgeCRBUkkpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgU2lsID0gcHVycnI6Om1hcF9kYmwoTG91dmFpbl9yZXNfaW5jciwgXCh4KSB4JEFSSSkpICU+JSAKICAgICAgICAgICAgICAgICAgICAgIGFycmFuZ2UoZGVzYyhBUkkpKQpsb3V2YWluX2FyaV90YWJsZSA8LSBncmlkRXh0cmE6OnRhYmxlR3JvYihsb3V2YWluX21ldHJpY19yZXMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbHMgPSBjKCJSZXNvbHV0aW9uIiwgIkFkai4gUmFuZCBJbmRleCIsICJTaWxob3VldHRlIiksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aGVtZSA9IGdyaWRFeHRyYTo6dHRoZW1lX21pbmltYWwoY29saGVhZCA9IGxpc3QoYmdfcGFyYW1zID0gbGlzdChmaWxsID0gImdyZXk5MCIpKSksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByb3dzID0gTlVMTCkKYGBgCgpXZSBzZWUgdGhhdCB0aGUgTG91dmFpbiBhbGdvcml0aG0gdGVuZHMgdG8gc3BsaXQgb2ZmIGxhcmdlciBjbHVzdGVycyBmaXJzdCBiZWZvcmUgaWRlbnRpZnlpbmcgdGhlIHRydWUgc3BsaXQgYmV0d2VlbiB0aGUgdHdvIHNtYWxsZXIgY2x1c3RlcnMgKHRydWUgY2x1c3RlcnMgNSAmIDYpLiAKCmBgYHtyLCBmaWcud2lkdGg9MTIsIGZpZy5oZWlnaHQ9MTB9CnA4YiA8LSBnZ3Bsb3QocmVzX2RmLCBhZXMoeCA9IHRTTkVfMSwgeSA9IHRTTkVfMiwgY29sb3IgPSBjbHVzdCkpICsgCiAgICAgICBmYWNldF93cmFwKH5wYXN0ZTAoIlJlc29sdXRpb246ICIsIHJlc29sdXRpb24pKSArIAogICAgICAgZ2VvbV9wb2ludChzaXplID0gMC43NSkgKyAKICAgICAgIHNjYWxlX2NvbG9yX3BhbGV0dGVlcl9kKCJnZ3NjaTo6Y2F0ZWdvcnkxMF9kMyIpICsgCiAgICAgICBsYWJzKHggPSAidC1TTkUgMSIsIAogICAgICAgICAgICB5ID0gInQtU05FIDIiLCAgCiAgICAgICAgICAgIGNvbG9yID0gIkxvdXZhaW5cbkNsdXN0ZXIiKSArIAogICAgICAgdGhlbWVfY2xhc3NpYyhiYXNlX3NpemUgPSAxNCkgKyAKICAgICAgIHRoZW1lKGF4aXMudGlja3MgPSBlbGVtZW50X2JsYW5rKCksIAogICAgICAgICAgICAgYXhpcy50ZXh0ID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgIHBsb3QudGl0bGUgPSBlbGVtZW50X2JsYW5rKCksIAogICAgICAgICAgICAgbGVnZW5kLnRpdGxlID0gZWxlbWVudF90ZXh0KGZhY2UgPSAiYm9sZCIpKSArIAogICAgICAgZ3VpZGVzKGNvbG9yID0gZ3VpZGVfbGVnZW5kKG92ZXJyaWRlLmFlcyA9IGxpc3Qoc2l6ZSA9IDMpKSkKKHA4YiAvIChwNiB8IGxvdXZhaW5fYXJpX3RhYmxlKSkgKyBwbG90X2xheW91dChoZWlnaHRzID0gYygyLCAxKSkgICAgICAKYGBgCgojIyBDb21wdXRhdGlvbmFsIENvc3QgCgpOZXh0LCBsZXQncyB0YWtlIGEgbG9vayBhdCBob3cgdGhlIHJ1bnRpbWVzIG9mIHRoZSB2YXJpb3VzIG1ldGhvZHMgY29tcGFyZS4gYHtTQ0lTU09SU31gIGhhcyB0aGUgaGlnaGVzdCBydW50aW1lLCBhcyBleHBlY3RlZCBiZWNhdXNlIGl0IHBlcmZvcm1zIG11bHRpcGxlIHJ1bnMgb2YgdGhlIExvdXZhaW4gYWxnb3JpdGhtLCBhcyBvcHBvc2VkIHRvIG1vc3Qgb2YgdGhlIHNpbXBsZXIgbWV0aG9kcyB3aGljaCBhcmUgc2luZ2xlLXBhc3MuIAoKYGBge3IsIGZpZy53aWR0aD0xMH0KcDlhIDwtIHNpbV9yZXN1bHRzICU+JSAKICAgICAgIG11dGF0ZShkYXRhc2V0X3NpemUgPSBzdHJpbmdyOjpzdHJfZXh0cmFjdChkYXRhc2V0LCAibmNlbGwuKl8iKSwgCiAgICAgICAgICAgICAgZGF0YXNldF9zaXplID0gc3RyaW5ncjo6c3RyX3JlcGxhY2Uoc3RyaW5ncjo6c3RyX3JlcGxhY2UoZGF0YXNldF9zaXplLCAibmNlbGwiLCAiIiksICJfIiwgIiIpLCAKICAgICAgICAgICAgICBkYXRhc2V0X3NpemUgPSBhcy5pbnRlZ2VyKGRhdGFzZXRfc2l6ZSksIAogICAgICAgICAgICAgIGRhdGFzZXRfc2l6ZSA9IGZhY3RvcihkYXRhc2V0X3NpemUsIGxldmVscyA9IGMoIjEwMDAiLCAiMzAwMCIsICI1MDAwIiwgIjEwMDAwIikpKSAlPiUgCiAgICAgICBnZ3Bsb3QoYWVzKHggPSBtZXRob2QsIHkgPSBydW50aW1lX21pbnV0ZXMsIGZpbGwgPSBtZXRob2QpKSArIAogICAgICAgZmFjZXRfd3JhcCh+bWV0aG9kLCBzY2FsZXMgPSAiZnJlZV94IikgKyAKICAgICAgIGdlb21fdmlvbGluKGRyYXdfcXVhbnRpbGVzID0gMC41LCAKICAgICAgICAgICAgICAgICAgIHNjYWxlID0gIndpZHRoIiwgCiAgICAgICAgICAgICAgICAgICBzaXplID0gMSwgCiAgICAgICAgICAgICAgICAgICBjb2xvciA9ICJibGFjayIpICsgCiAgICAgICBzY2FsZV95X2NvbnRpbnVvdXModHJhbnMgPSAibG9nIiwgbGFiZWxzID0gc2NhbGVzOjpudW1iZXJfZm9ybWF0KGFjY3VyYWN5ID0gLjEpKSArIAogICAgICAgc2NhbGVfZmlsbF9wYWxldHRlZXJfZCgiZ2dzY2k6Om5yY19ucGciKSArIAogICAgICAgbGFicyh5ID0gImxvZyhSdW50aW1lKSIsIGZpbGwgPSAiQ2x1c3RlcmluZyBNZXRob2QiKSArIAogICAgICAgdGhlbWVfY2xhc3NpYyhiYXNlX3NpemUgPSAxNCkgKyAKICAgICAgIHRoZW1lKGF4aXMudGlja3MueCA9IGVsZW1lbnRfYmxhbmsoKSwgCiAgICAgICAgICAgICBwYW5lbC5ncmlkLm1ham9yLnkgPSBlbGVtZW50X2xpbmUoKSwgCiAgICAgICAgICAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCkpCnA5YQpgYGAKCldlJ2xsIGFsc28gcGxvdCB1bi10cmFuc2Zvcm1lZCBydW50aW1lLCB0aG91Z2ggdGhlIHBsb3QgaXMgaGFyZGVyIHRvIHJlYWQgYmVjYXVzZSBvZiB0aGUgZGlzcGFyaXR5IGluIHJ1bnRpbWVzIGJldHdlZW4gc2ltcGxlciBhbmQgbW9yZSBjb21wbGV4IG1ldGhvZHMuIEl0J3MgZXhwZWN0ZWQgdGhhdCBge1NDSVNTT1JTfWAgaGF2ZSBsb25nZXIgcnVudGltZXMgYXMgaXQncyBhIG11bHRpLXBhc3MgYWxnb3JpdGhtLCBidXQgZXZlbiBmb3IgbGFyZ2VyIGRhdGFzZXRzIGl0IHJhcmVseSBleGNlZWRzIDEwIG1pbnV0ZXMgb2YgcnVudGltZSB0aGFua3MgdG8gYXV0b21hdGljIGlkZW50aWZpY2F0aW9uIG9mIHJlY2x1c3RlcmluZyB0YXJnZXRzLiAKCmBgYHtyLCBmaWcud2lkdGg9MTJ9CnA5YiA8LSBzaW1fcmVzdWx0cyAlPiUgCiAgICAgICBtdXRhdGUoZGF0YXNldF9zaXplID0gc3RyaW5ncjo6c3RyX2V4dHJhY3QoZGF0YXNldCwgIm5jZWxsLipfIiksIAogICAgICAgICAgICAgIGRhdGFzZXRfc2l6ZSA9IHN0cmluZ3I6OnN0cl9yZXBsYWNlKHN0cmluZ3I6OnN0cl9yZXBsYWNlKGRhdGFzZXRfc2l6ZSwgIm5jZWxsIiwgIiIpLCAiXyIsICIiKSwgCiAgICAgICAgICAgICAgZGF0YXNldF9zaXplID0gYXMuaW50ZWdlcihkYXRhc2V0X3NpemUpLCAKICAgICAgICAgICAgICBkYXRhc2V0X3NpemUgPSBmYWN0b3IoZGF0YXNldF9zaXplLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGV2ZWxzID0gYygxMDAwTCwgMzAwMEwsIDUwMDBMLCAxMDAwMEwpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGFiZWxzID0gYygiMSwwMDAiLCAiMywwMDAiLCAiNSwwMDAiLCAiMTAsMDAwIikpKSAlPiUgCiAgICAgICBnZ3Bsb3QoYWVzKHggPSBkYXRhc2V0X3NpemUsIHkgPSBydW50aW1lX21pbnV0ZXMsIGZpbGwgPSBtZXRob2QsIGdyb3VwID0gZGF0YXNldF9zaXplKSkgKyAKICAgICAgIGZhY2V0X3dyYXAofm1ldGhvZCwgc2NhbGVzID0gImZyZWUiKSArIAogICAgICAgZ2VvbV92aW9saW4oZHJhd19xdWFudGlsZXMgPSAwLjUsIAogICAgICAgICAgICAgICAgICAgcG9zaXRpb24gPSBwb3NpdGlvbl9kb2RnZSgxKSwgCiAgICAgICAgICAgICAgICAgICBzY2FsZSA9ICJ3aWR0aCIsIAogICAgICAgICAgICAgICAgICAgc2l6ZSA9IDEsIAogICAgICAgICAgICAgICAgICAgY29sb3IgPSAiYmxhY2siKSArIAogICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6bnVtYmVyX2Zvcm1hdChhY2N1cmFjeSA9IC4wMSkpICsgCiAgICAgICBzY2FsZV9maWxsX3BhbGV0dGVlcl9kKCJnZ3NjaTo6bnJjX25wZyIpICsgCiAgICAgICBsYWJzKHggPSAiTnVtYmVyIG9mIENlbGxzIiwgCiAgICAgICAgICAgIHkgPSAiUnVudGltZSAobWludXRlcykiLCAKICAgICAgICAgICAgZmlsbCA9ICJDbHVzdGVyaW5nIE1ldGhvZCIpICsgCiAgICAgICB0aGVtZV9jbGFzc2ljKGJhc2Vfc2l6ZSA9IDE0KSArIAogICAgICAgdGhlbWUoYXhpcy50aWNrcy54ID0gZWxlbWVudF9ibGFuaygpLCAKICAgICAgICAgICAgIHBhbmVsLmdyaWQubWFqb3IueSA9IGVsZW1lbnRfbGluZSgpKQpwOWIKYGBgCgojIFNhdmUgRmlndXJlcwoKRmlyc3Qgd2UnbGwgZGVmaW5lIGEgY29udmVuaWVuY2UgZnVuY3Rpb24gdG8gaGVscCBzYXZlIG91ciBwbG90cy4gCgpgYGB7cn0KZmlnX3NhdmUgPC0gZnVuY3Rpb24ocGxvdC5vYmosIHBsb3QubmFtZSA9ICIiLCBkaW1zID0gYyg5LCA1KSkgewogIGdncGxvdDI6Omdnc2F2ZShmaWxlbmFtZSA9IHBsb3QubmFtZSwgCiAgICAgICAgICAgICAgICAgIHBhdGggPSAiLi9maWd1cmVzIiwgCiAgICAgICAgICAgICAgICAgIHBsb3QgPSBwbG90Lm9iaiwgCiAgICAgICAgICAgICAgICAgIGRldmljZSA9ICJwZGYiLCAKICAgICAgICAgICAgICAgICAgd2lkdGggPSBkaW1zWzFdLCAKICAgICAgICAgICAgICAgICAgaGVpZ2h0ID0gZGltc1syXSwgCiAgICAgICAgICAgICAgICAgIHVuaXRzID0gImluIiwgCiAgICAgICAgICAgICAgICAgIGRwaSA9ICJyZXRpbmEiKQp9CmBgYAoKTm93IGxldCdzIHNhdmUgdGhlbSBhbGwgYXMgUERGcy4gCgpgYGB7cn0KZmlnX3NhdmUocDBhLCBwbG90Lm5hbWUgPSAiVG90YWxfQVJJX0J5X01ldGhvZF9BbmRfUmVmZXJlbmNlLnBkZiIsIGRpbXMgPSBjKDEwLCA2KSkKZmlnX3NhdmUocDBiLCBwbG90Lm5hbWUgPSAiVG90YWxfQVJJX0J5X01ldGhvZC5wZGYiLCBkaW1zID0gYygxMCwgNikpCmZpZ19zYXZlKHAxYSwgcGxvdC5uYW1lID0gIlRvdGFsX05NSV9CeV9NZXRob2RfQW5kX1JlZmVyZW5jZS5wZGYiLCBkaW1zID0gYygxMCwgNikpCmZpZ19zYXZlKHAxYiwgcGxvdC5uYW1lID0gIlRvdGFsX05NSV9CeV9NZXRob2QucGRmIiwgZGltcyA9IGMoMTAsIDYpKQpmaWdfc2F2ZShwMmEsIHBsb3QubmFtZSA9ICJUb3RhbF9TaWxob3VldHRlX0J5X01ldGhvZF9BbmRfUmVmZXJlbmNlLnBkZiIsIGRpbXMgPSBjKDEwLCA2KSkKZmlnX3NhdmUocDJiLCBwbG90Lm5hbWUgPSAiVG90YWxfU2lsaG91ZXR0ZV9CeV9NZXRob2QucGRmIiwgZGltcyA9IGMoMTAsIDYpKQpmaWdfc2F2ZShwMywgcGxvdC5uYW1lID0gIkFib3ZlX01lZGlhbl9BUklfQnlfTWV0aG9kX0FuZF9SZWZlcmVuY2UucGRmIiwgZGltcyA9IGMoMTAsIDYpKQpmaWdfc2F2ZShwNCwgcGxvdC5uYW1lID0gIkFib3ZlX01lZGlhbl9OTUlfQnlfTWV0aG9kX0FuZF9SZWZlcmVuY2UucGRmIiwgZGltcyA9IGMoMTAsIDYpKQpmaWdfc2F2ZShwNSwgcGxvdC5uYW1lID0gIkFib3ZlX01lZGlhbl9TaWxob3VldHRlX0J5X01ldGhvZF9BbmRfUmVmZXJlbmNlLnBkZiIsIGRpbXMgPSBjKDEwLCA2KSkKZmlnX3NhdmUocDYsIHBsb3QubmFtZSA9ICJDYXNlX1N0dWR5X1RydWVfTGFiZWxfdFNORS5wZGYiLCBkaW1zID0gYyg4LCA0LjUpKQpmaWdfc2F2ZShwNywgcGxvdC5uYW1lID0gIkNhc2VfU3R1ZHlfRXN0aW1hdGVkX0NsdXN0ZXJzX0FsbF90U05FLnBkZiIsIGRpbXMgPSBjKDEyLCA3KSkKZmlnX3NhdmUocDhhLCBwbG90Lm5hbWUgPSAiQ2FzZV9TdHVkeV9Fc3RpbWF0ZWRfQ2x1c3RlcnNfUGVyZm9ybWFuY2UucGRmIiwgZGltcyA9IGMoMTUsIDgpKQpmaWdfc2F2ZShwOGIsIHBsb3QubmFtZSA9ICJDYXNlX1N0dWR5X0xvdXZhaW5fUmVzb2x1dGlvbl9QZXJmb3JtYW5jZS5wZGYiLCBkaW1zID0gYygxNSwgOCkpCmZpZ19zYXZlKHA5YSwgcGxvdC5uYW1lID0gIlJ1bnRpbWVfX0xvZ19CeV9NZXRob2RfQW5kX1JlZmVyZW5jZS5wZGYiLCBkaW1zID0gYygxMiwgNykpCmZpZ19zYXZlKHA5YiwgcGxvdC5uYW1lID0gIlJ1bnRpbWVfUmF3X0J5X01ldGhvZF9BbmRfTnVtYmVyX0NlbGxzLnBkZiIsIGRpbXMgPSBjKDEyLCA3KSkKYGBgCgojIFNlc3Npb24gSW5mbwoKYGBge3J9CnNlc3Npb25pbmZvOjpzZXNzaW9uX2luZm8oKQpgYGAK